From 1c45331b0aadd9240e595381ece68f96d6dda19f Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 14 Oct 2021 20:35:42 +0100
Subject: [PATCH] Fix and optimise code (#904)
* Fix bug in early error reporting in main init
* Update docstrings and optimise code in GuiMain
* Update docstrings and optimise code in Config
* Update docstrings and optimise code in common module
* Update docstrings and optimise code in main project classes
* Update the OptionState class
* Update the spell checker class
* Update the file converter classes and extend tests
* Update the about, merge, split and item editor classes and extend tests
* Update item editor test
* Update about dialog tests
* Some minor test cleanup
* Fix typo and add clarification in contributing guide
---
CONTRIBUTING.md | 4 +-
novelwriter/__init__.py | 20 +-
novelwriter/common.py | 37 ++-
novelwriter/config.py | 95 ++++--
novelwriter/core/index.py | 19 +-
novelwriter/core/item.py | 21 +-
novelwriter/core/options.py | 200 +++++--------
novelwriter/core/project.py | 17 +-
novelwriter/core/spellcheck.py | 68 +++--
novelwriter/core/tohtml.py | 122 ++++----
novelwriter/core/tokenizer.py | 9 +-
novelwriter/core/tomd.py | 24 +-
novelwriter/core/toodt.py | 77 ++---
novelwriter/core/tree.py | 10 +-
novelwriter/dialogs/about.py | 12 +-
novelwriter/dialogs/docmerge.py | 2 +-
novelwriter/dialogs/docsplit.py | 18 +-
novelwriter/dialogs/itemeditor.py | 3 +-
novelwriter/gui/doceditor.py | 2 +-
novelwriter/guimain.py | 38 ++-
.../guiItemEditor_Dialog_nwProject.nwx | 115 --------
tests/test_base/test_base_common.py | 18 +-
tests/test_core/test_core_options.py | 6 +-
tests/test_core/test_core_spell.py | 2 +-
tests/test_core/test_core_tohtml.py | 110 +++----
tests/test_core/test_core_tomd.py | 277 ++++++++++++++++++
tests/test_core/test_core_toodt.py | 86 ++++++
tests/test_dialogs/test_dlg_about.py | 52 ++--
...test_dlg_merge.py => test_dlg_docmerge.py} | 6 +-
...test_dlg_split.py => test_dlg_docsplit.py} | 8 +-
tests/test_dialogs/test_dlg_itemeditor.py | 230 +++++++++++----
31 files changed, 1042 insertions(+), 666 deletions(-)
delete mode 100644 tests/reference/guiItemEditor_Dialog_nwProject.nwx
create mode 100644 tests/test_core/test_core_tomd.py
rename tests/test_dialogs/{test_dlg_merge.py => test_dlg_docmerge.py} (98%)
rename tests/test_dialogs/{test_dlg_split.py => test_dlg_docsplit.py} (99%)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6e1173fe..c2fc9cc8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -76,13 +76,13 @@ style guide, but with a few exceptions. Some key points are listed below.
**Variable and Function Names**
* PEP8 allows for camelCase for consistency with existing code. The Qt library uses camelCase, so
- the Python the source code does too.
+ the Python source code does too.
* The exception to the above is for constants. They should always be in upper snake case, like PEP8
states.
**Spaces, Indentation and Alignment**
-* Only indentation by 4 spaces is allowed.
+* Only indentation by multiples of 4 spaces is allowed.
* No trailing spaces should occur on any line in the source code, including empty lines.
* Ideally, a function should end on the same indention level as it started. Exceptions are allowed
if it makes the code easier to follow.
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 74bbaaa0..4f0f376e 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -117,7 +117,7 @@ def main(sysArgs=None):
# Valid Input Options
shortOpt = "hv"
- longOpt = [
+ longOpt = [
"help",
"version",
"info",
@@ -215,31 +215,31 @@ def main(sysArgs=None):
errorCode = 0
if sys.hexversion < 0x030600f0:
errorData.append(
- "At least Python 3.6.0 is required, found %s." % CONFIG.verPyString
+ "At least Python 3.6.0 is required, found %s" % CONFIG.verPyString
)
errorCode |= 4
if CONFIG.verQtValue < 50300:
errorData.append(
- "At least Qt5 version 5.3 is required, found %s." % CONFIG.verQtString
+ "At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
)
errorCode |= 8
if CONFIG.verPyQtValue < 50300:
errorData.append(
- "At least PyQt5 version 5.3 is required, found %s." % CONFIG.verPyQtString
+ "At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
)
errorCode |= 16
try:
import lxml # noqa: F401
except ImportError:
- errorData.append("Python module 'lxml' is missing.")
+ errorData.append("Python module 'lxml' is missing")
errorCode |= 32
if errorData:
errApp = QApplication([])
- errMsg = QErrorMessage()
- errMsg.resize(500, 300)
- errMsg.showMessage((
+ errDlg = QErrorMessage()
+ errDlg.resize(500, 300)
+ errDlg.showMessage((
"
A critical error has been encountered
"
"novelWriter cannot start due to the following issues:
"
"
- %s
"
@@ -247,8 +247,8 @@ def main(sysArgs=None):
) % (
"
- ".join(errorData)
))
- for errMsg in errorData:
- logger.critical(errMsg)
+ for errLine in errorData:
+ logger.critical(errLine)
errApp.exec_()
sys.exit(errorCode)
diff --git a/novelwriter/common.py b/novelwriter/common.py
index 7e699e97..92ab66c6 100644
--- a/novelwriter/common.py
+++ b/novelwriter/common.py
@@ -46,7 +46,7 @@ logger = logging.getLogger(__name__)
# =============================================================================================== #
def checkString(value, default, allowNone=False):
- """Check if a variable is a string or a none.
+ """Check if a variable is a string or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -56,7 +56,7 @@ def checkString(value, default, allowNone=False):
def checkInt(value, default, allowNone=False):
- """Check if a variable is an integer or a none.
+ """Check if a variable is an integer or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -66,8 +66,19 @@ def checkInt(value, default, allowNone=False):
return default
+def checkFloat(value, default, allowNone=False):
+ """Check if a variable is a float or a None.
+ """
+ if allowNone and (value is None or value == "None"):
+ return None
+ try:
+ return float(value)
+ except Exception:
+ return default
+
+
def checkBool(value, default, allowNone=False):
- """Check if a variable is a boolean or a none.
+ """Check if a variable is a boolean or a None.
"""
if allowNone and (value is None or value == "None"):
return None
@@ -211,7 +222,7 @@ def formatTime(tS):
def parseTimeStamp(theStamp, default, allowNone=False):
- """Parses a text representation of a time stamp and converts it into
+ """Parses a text representation of a timestamp and converts it into
a float. Note that negative timestamps cause an OSError on Windows.
See https://bugs.python.org/issue29097
"""
@@ -228,7 +239,7 @@ def parseTimeStamp(theStamp, default, allowNone=False):
# =============================================================================================== #
def splitVersionNumber(value):
- """Splits a version string on the form aa.bb.cc into major, minor
+ """Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
"""
if not isinstance(value, str):
@@ -337,7 +348,7 @@ def fuzzyTime(secDiff):
).format(int(round(secDiff/31557600)))
-def numberToRoman(numVal, isLower=False):
+def numberToRoman(numVal, toLower=False):
"""Convert an integer to a Roman number.
"""
if not isinstance(numVal, int):
@@ -358,7 +369,7 @@ def numberToRoman(numVal, isLower=False):
if numVal <= 0:
break
- return romNum.lower() if isLower else romNum
+ return romNum.lower() if toLower else romNum
# =============================================================================================== #
@@ -434,11 +445,11 @@ def readTextFile(filePath):
return fileText
-def makeFileNameSafe(theText):
- """Returns a filename safe version of the text.
+def makeFileNameSafe(value):
+ """Returns a filename safe string of the value.
"""
cleanName = ""
- for c in theText.strip():
+ for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
@@ -456,7 +467,7 @@ def sha256sum(filePath):
for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n])
except Exception:
- logger.error("Could not read sha256sum of: %s", filePath)
+ logger.error("Could not create sha256sum of: %s", filePath)
logException()
return None
@@ -467,11 +478,11 @@ def sha256sum(filePath):
# Other Functions
# =============================================================================================== #
-def getGuiItem(theName):
+def getGuiItem(objName):
"""Returns a QtWidget based on its objectName.
"""
for qWidget in qApp.topLevelWidgets():
- if qWidget.objectName() == theName:
+ if qWidget.objectName() == objName:
return qWidget
return None
diff --git a/novelwriter/config.py b/novelwriter/config.py
index 8a7231f8..6b342acc 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -302,7 +302,7 @@ class Config:
logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath)
- # If config folder does not exist, create it.
+ # If the config folder does not exist, create it.
# This assumes that the os config folder itself exists.
if not os.path.isdir(self.confPath):
try:
@@ -324,7 +324,7 @@ class Config:
# If it does not exist, save a copy of the default values
self.saveConfig()
- # If data folder does not exist, make it.
+ # If the data folder does not exist, create it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not os.path.isdir(self.dataPath):
@@ -534,7 +534,7 @@ class Config:
logger.info("Using straight single quotes, so disabling auto-replace")
self.doReplaceSQuote = False
- if self.fmtDoubleQuotes == ["\"", "\""] and self.doReplaceDQuote:
+ if self.fmtDoubleQuotes == ['"', '"'] and self.doReplaceDQuote:
logger.info("Using straight double quotes, so disabling auto-replace")
self.doReplaceDQuote = False
@@ -671,36 +671,28 @@ class Config:
if self.dataPath is None:
return False
- cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentProj = {}
- if os.path.isfile(cacheFile):
- try:
- with open(cacheFile, mode="r", encoding="utf-8") as inFile:
- theData = json.load(inFile)
+ cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE)
+ if not os.path.isfile(cacheFile):
+ return True
- for projPath in theData.keys():
- theEntry = theData[projPath]
- theTitle = ""
- lastTime = 0
- wordCount = 0
- if "title" in theEntry.keys():
- theTitle = theEntry["title"]
- if "time" in theEntry.keys():
- lastTime = int(theEntry["time"])
- if "words" in theEntry.keys():
- wordCount = int(theEntry["words"])
- self.recentProj[projPath] = {
- "title": theTitle,
- "time": lastTime,
- "words": wordCount,
- }
+ try:
+ with open(cacheFile, mode="r", encoding="utf-8") as inFile:
+ theData = json.load(inFile)
- except Exception as e:
- self.hasError = True
- self.errData.append("Could not load recent project cache")
- self.errData.append(str(e))
- return False
+ 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 e:
+ self.hasError = True
+ self.errData.append("Could not load recent project cache")
+ self.errData.append(str(e))
+ return False
return True
@@ -755,6 +747,8 @@ class Config:
##
def setConfPath(self, newPath):
+ """Set the path and filename to the config file.
+ """
if newPath is None:
return True
if not os.path.isfile(newPath):
@@ -765,6 +759,8 @@ class Config:
return True
def setDataPath(self, newPath):
+ """Set the data path.
+ """
if newPath is None:
return True
if not os.path.isdir(newPath):
@@ -774,6 +770,8 @@ class Config:
return True
def setLastPath(self, lastPath):
+ """Set the last used path (by the user).
+ """
if lastPath is None or lastPath == "":
self.lastPath = ""
else:
@@ -781,6 +779,11 @@ class Config:
return True
def setWinSize(self, newWidth, newHeight):
+ """Set the size of the main window, but only if the change is
+ larger than 5 pixels. The OS window manager will sometimes
+ adjust it a bit, and we don't want the main window to shrink or
+ grow each time the app is opened.
+ """
newWidth = int(newWidth/self.guiScale)
newHeight = int(newHeight/self.guiScale)
if abs(self.winGeometry[0] - newWidth) > 5:
@@ -792,57 +795,79 @@ class Config:
return True
def setPreferencesSize(self, newWidth, newHeight):
+ """Sat the size of the Preferences dialog window.
+ """
self.prefGeometry[0] = int(newWidth/self.guiScale)
self.prefGeometry[1] = int(newHeight/self.guiScale)
self.confChanged = True
return True
def setTreeColWidths(self, colWidths):
+ """Set the column widths of the main project tree.
+ """
self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setNovelColWidths(self, colWidths):
+ """Set the column widths of the novel tree.
+ """
self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setProjColWidths(self, colWidths):
+ """Set the column widths of the Load Project dialog.
+ """
self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setMainPanePos(self, panePos):
+ """Set the position of the main GUI splitter.
+ """
self.mainPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setDocPanePos(self, panePos):
+ """Set the position of the main editor/viewer splitter.
+ """
self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setViewPanePos(self, panePos):
+ """Set the position of the viewer meta data splitter.
+ """
self.viewPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setOutlinePanePos(self, panePos):
+ """Set the position of the outline details splitter.
+ """
self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True
return True
def setShowRefPanel(self, checkState):
+ """Set the visibility state of the reference panel.
+ """
self.showRefPanel = checkState
self.confChanged = True
return self.showRefPanel
def setViewComments(self, viewState):
+ """Set the visibility state of comments in the viewer.
+ """
self.viewComments = viewState
self.confChanged = True
return self.viewComments
def setViewSynopsis(self, viewState):
+ """Set the visibility state of synopsis comments in the viewer.
+ """
self.viewSynopsis = viewState
self.confChanged = True
return self.viewSynopsis
@@ -852,12 +877,18 @@ class Config:
##
def setDefaultGuiTheme(self):
+ """Reset the GUI theme to default value.
+ """
self.guiTheme = "default"
def setDefaultSyntaxTheme(self):
+ """Reset the syntax theme to default value.
+ """
self.guiSyntax = "default_light"
def setDefaultIconTheme(self):
+ """Reset the icon theme to default value.
+ """
self.guiIcons = "typicons_light"
##
@@ -904,6 +935,9 @@ class Config:
return self.pxInt(self.focusWidth)
def getErrData(self):
+ """Compile and return error messages from the initialisation of
+ the Config class, and clear the error buffer.
+ """
errMessage = "
".join(self.errData)
self.hasError = False
self.errData = []
@@ -914,7 +948,8 @@ class Config:
##
def _packList(self, inData):
- """Pack a list of items into a comma-separated string.
+ """Pack a list of items into a comma-separated string for saving
+ to the config file.
"""
return ", ".join([str(inVal) for inVal in inData])
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 55839ff4..77e75b0c 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -287,10 +287,8 @@ class NWIndex():
nLine = 0
nTitle = 0
theLines = theText.splitlines()
- for aLine in theLines:
- nLine += 1
- nChar = len(aLine.strip())
- if nChar == 0:
+ for nLine, aLine in enumerate(theLines, start=1):
+ if len(aLine.strip()) == 0:
continue
if aLine.startswith("#"):
@@ -363,7 +361,7 @@ class NWIndex():
else:
return False
- sTitle = "T%06d" % nLine
+ sTitle = f"T{nLine:06d}"
self._fileIndex[tHandle][sTitle] = {
"level": hDepth,
"title": hText,
@@ -391,14 +389,13 @@ class NWIndex():
"pCount": 0,
"synopsis": "",
}
-
return
def _indexWordCounts(self, tHandle, theText, nTitle):
"""Count text stats and save the counts to the index.
"""
cC, wC, pC = countWords(theText)
- sTitle = "T%06d" % nTitle
+ sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["cCount"] = cC
@@ -409,7 +406,7 @@ class NWIndex():
def _indexSynopsis(self, tHandle, theText, nTitle):
"""Save the synopsis to the index.
"""
- sTitle = "T%06d" % nTitle
+ sTitle = f"T{nTitle:06d}"
if tHandle in self._fileIndex:
if sTitle in self._fileIndex[tHandle]:
self._fileIndex[tHandle][sTitle]["synopsis"] = theText
@@ -428,7 +425,7 @@ class NWIndex():
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return
- sTitle = "T%06d" % nTitle
+ sTitle = f"T{nTitle:06d}"
if theBits[0] == nwKeyWords.TAG_KEY:
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
@@ -525,7 +522,7 @@ class NWIndex():
"""
for tHandle in self._listNovelHandles(skipExcluded):
for sTitle in sorted(self._fileIndex[tHandle]):
- tKey = "%s:%s" % (tHandle, sTitle)
+ tKey = f"{tHandle}:{sTitle}"
yield tKey, tHandle, sTitle, self._fileIndex[tHandle][sTitle]
def getNovelWordCount(self, skipExcluded=True):
@@ -559,7 +556,7 @@ class NWIndex():
return theCounts
for sTitle, sData in hRecord.items():
- theCounts.append(("%s:%s" % (tHandle, sTitle), sData["wCount"]))
+ theCounts.append((f"{tHandle}:{sTitle}", sData["wCount"]))
return theCounts
diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py
index 3170c6cd..985a9e8c 100644
--- a/novelwriter/core/item.py
+++ b/novelwriter/core/item.py
@@ -103,11 +103,8 @@ class NWItem():
logger.error("XML item entry does not have a handle")
return False
- if "parent" in xItem.attrib:
- self.setParent(xItem.attrib["parent"])
-
- if "order" in xItem.attrib:
- self.setOrder(xItem.attrib["order"])
+ self.setParent(xItem.attrib.get("parent", None))
+ self.setOrder(xItem.attrib.get("order", 0))
tmpStatus = ""
for xValue in xItem:
@@ -200,11 +197,8 @@ class NWItem():
def setHandle(self, theHandle):
"""Set the item handle, and ensure it is valid.
"""
- if isinstance(theHandle, str):
- if isHandle(theHandle):
- self.itemHandle = theHandle
- else:
- self.itemHandle = None
+ if isHandle(theHandle):
+ self.itemHandle = theHandle
else:
self.itemHandle = None
return
@@ -214,11 +208,8 @@ class NWItem():
"""
if theParent is None:
self.itemParent = None
- elif isinstance(theParent, str):
- if isHandle(theParent):
- self.itemParent = theParent
- else:
- self.itemParent = None
+ elif isHandle(theParent):
+ self.itemParent = theParent
else:
self.itemParent = None
return
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 3526d4f8..424fe659 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -30,88 +30,40 @@ import logging
import novelwriter
from novelwriter.constants import nwFiles
+from novelwriter.common import checkBool, checkFloat, checkInt, checkString
logger = logging.getLogger(__name__)
+VALID_MAP = {
+ "GuiWritingStats": {
+ "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
+ "widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
+ "hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
+ },
+ "GuiDocSplit": {"spLevel"},
+ "GuiBuildNovel": {
+ "winWidth", "winHeight", "boxWidth", "docWidth", "addNovel",
+ "addNotes", "ignoreFlag", "justifyText", "excludeBody", "textFont",
+ "textSize", "lineHeight", "noStyling", "incSynopsis", "incComments",
+ "incKeywords", "incBodyText", "replaceTabs", "replaceUCode"
+ },
+ "GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
+ "GuiProjectSettings": {
+ "winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
+ },
+ "GuiProjectDetails": {
+ "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
+ "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
+ },
+ "GuiWordList": {"winWidth", "winHeight"}
+}
+
class OptionState():
def __init__(self, theProject):
-
self.theProject = theProject
- self.theState = {}
- self.validMap = {
- "GuiWritingStats": {
- "winWidth",
- "winHeight",
- "widthCol0",
- "widthCol1",
- "widthCol2",
- "widthCol3",
- "sortCol",
- "sortOrder",
- "incNovel",
- "incNotes",
- "hideZeros",
- "hideNegative",
- "groupByDay",
- "showIdleTime",
- "histMax",
- },
- "GuiDocSplit": {
- "spLevel",
- },
- "GuiBuildNovel": {
- "winWidth",
- "winHeight",
- "boxWidth",
- "docWidth",
- "addNovel",
- "addNotes",
- "ignoreFlag",
- "justifyText",
- "excludeBody",
- "textFont",
- "textSize",
- "lineHeight",
- "noStyling",
- "incSynopsis",
- "incComments",
- "incKeywords",
- "incBodyText",
- "replaceTabs",
- "replaceUCode",
- },
- "GuiOutline": {
- "headerOrder",
- "columnWidth",
- "columnHidden",
- },
- "GuiProjectSettings": {
- "winWidth",
- "winHeight",
- "replaceColW",
- "statusColW",
- "importColW",
- },
- "GuiProjectDetails": {
- "winWidth",
- "winHeight",
- "widthCol0",
- "widthCol1",
- "widthCol2",
- "widthCol3",
- "widthCol4",
- "wordsPerPage",
- "countFrom",
- "clearDouble",
- },
- "GuiWordList": {
- "winWidth",
- "winHeight",
- }
- }
-
+ self._theState = {}
return
##
@@ -139,11 +91,11 @@ class OptionState():
# Filter out unused variables
for aGroup in theState:
- if aGroup in self.validMap:
- self.theState[aGroup] = {}
+ if aGroup in VALID_MAP:
+ self._theState[aGroup] = {}
for anOpt in theState[aGroup]:
- if anOpt in self.validMap[aGroup]:
- self.theState[aGroup][anOpt] = theState[aGroup][anOpt]
+ if anOpt in VALID_MAP[aGroup]:
+ self._theState[aGroup][anOpt] = theState[aGroup][anOpt]
return True
@@ -158,7 +110,7 @@ class OptionState():
try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
- json.dump(self.theState, outFile, indent=2)
+ json.dump(self._theState, outFile, indent=2)
except Exception:
logger.error("Failed to save GUI options file")
novelwriter.logException()
@@ -170,21 +122,21 @@ class OptionState():
# Setters
##
- def setValue(self, setGroup, setName, setValue):
+ def setValue(self, group, name, value):
"""Saves a value, with a given group and name.
"""
- if setGroup not in self.validMap:
- logger.error("Unknown option group '%s'", setGroup)
+ if group not in VALID_MAP:
+ logger.error("Unknown option group '%s'", group)
return False
- if setName not in self.validMap[setGroup]:
- logger.error("Unknown option name '%s'", setName)
+ if name not in VALID_MAP[group]:
+ logger.error("Unknown option name '%s'", name)
return False
- if setGroup not in self.theState:
- self.theState[setGroup] = {}
+ if group not in self._theState:
+ self._theState[group] = {}
- self.theState[setGroup][setName] = setValue
+ self._theState[group][name] = value
return True
@@ -192,79 +144,67 @@ class OptionState():
# Getters
##
- def getValue(self, getGroup, getName, defaultValue):
+ def getValue(self, group, name, default):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- return self.theState[getGroup][getName]
- return defaultValue
+ if group in self._theState:
+ return self._theState[group].get(name, default)
+ return default
- def getString(self, getGroup, getName, defaultValue):
+ def getString(self, group, name, default):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
"""
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- return str(self.theState[getGroup][getName])
- return defaultValue
+ if group in self._theState:
+ return checkString(self._theState[group].get(name, default), default)
+ return default
- def getInt(self, getGroup, getName, defaultValue):
+ def getInt(self, group, name, default):
"""Return the value as an int, if it exists. Otherwise, return
the default value.
"""
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return int(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
+ if group in self._theState:
+ return checkInt(self._theState[group].get(name, default), default)
+ return default
- def getFloat(self, getGroup, getName, defaultValue):
+ def getFloat(self, group, name, default):
"""Return the value as a float, if it exists. Otherwise, return
the default value.
"""
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- try:
- return float(self.theState[getGroup][getName])
- except Exception as e:
- logger.warning(str(e))
- return defaultValue
- return defaultValue
+ if group in self._theState:
+ return checkFloat(self._theState[group].get(name, default), default)
+ return default
- def getBool(self, getGroup, getName, defaultValue):
+ def getBool(self, group, name, default):
"""Return the value as a bool, if it exists. Otherwise, return
the default value.
"""
- if getGroup in self.theState:
- if getName in self.theState[getGroup]:
- return bool(self.theState[getGroup][getName])
- return defaultValue
+ if group in self._theState:
+ if name in self._theState[group]:
+ return checkBool(self._theState[group].get(name, default), default)
+ return default
##
# Validators
##
- def validIntRange(self, theValue, intA, intB, intDefault):
+ def validIntRange(self, value, first, last, default):
"""Check that an int is in a given range. If it isn't, return
the default value.
"""
- if isinstance(theValue, int):
- if theValue >= intA and theValue <= intB:
- return theValue
- return intDefault
+ if isinstance(value, int):
+ if value >= first and value <= last:
+ return value
+ return default
- def validIntTuple(self, theValue, theTuple, intDefault):
+ def validIntTuple(self, value, valid, default):
"""Check that an int is an element of a tuple. If it isn't,
return the default value.
"""
- if isinstance(theValue, int):
- if theValue in theTuple:
- return theValue
- return intDefault
+ if isinstance(value, int):
+ if value in valid:
+ return value
+ return default
# END Class OptionState
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index a1a70bda..98050051 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -125,6 +125,7 @@ class NWProject():
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
+
newItem = NWItem(self)
newItem.setName(rootName)
newItem.setType(nwItemType.ROOT)
@@ -356,7 +357,7 @@ class NWProject():
return True
def openProject(self, fileName, overrideLock=False):
- """Open the project file provided, or if doesn't exist, assume
+ """Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
@@ -469,8 +470,8 @@ class NWProject():
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
# 1.3 : Reduces the number of layouts to onlye two. One for
- # novel documents and one for notes. Introduced in version
- # 1.5.
+ # novel documents and one for project notes. Introduced in
+ # version 1.5.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3"):
self.theParent.makeAlert(self.tr(
@@ -973,7 +974,7 @@ class NWProject():
return False
self.bookAuthors = []
- for bookAuthor in bookAuthors.split("\n"):
+ for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip()
if bookAuthor == "":
continue
@@ -1074,7 +1075,7 @@ class NWProject():
replaceMap = self.statusItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass == nwItemClass.NOVEL:
- if nwItem.itemStatus in replaceMap.keys():
+ if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1086,7 +1087,7 @@ class NWProject():
replaceMap = self.importItems.setNewEntries(newCols)
for nwItem in self.projTree:
if nwItem.itemClass != nwItemClass.NOVEL:
- if nwItem.itemStatus in replaceMap.keys():
+ if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
@@ -1104,7 +1105,7 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
- self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey], False)
+ self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
return True
def setProjectChanged(self, bValue):
@@ -1346,7 +1347,7 @@ class NWProject():
return
def _packProjectKeyValue(self, xParent, theName, theDict):
- """Pack the entries in the auto-replace dictionary.
+ """Pack the entries of a dictionary into an xml element.
"""
xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items():
diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py
index 580326b1..dcdbe089 100644
--- a/novelwriter/core/spellcheck.py
+++ b/novelwriter/core/spellcheck.py
@@ -35,16 +35,24 @@ class NWSpellEnchant():
def __init__(self):
self.mainConf = novelwriter.CONFIG
- self.theDict = None
- self.projDict = set()
- self.projectDict = None
- self.spellLanguage = None
- self.theBroker = None
+
+ self._theDict = None
+ self._projDict = set()
+ self._projectDict = None
+ self._spellLanguage = None
+ self._theBroker = None
logger.debug("Enchant spell checking activated")
return
+ ##
+ # Getters and Setters
+ ##
+
+ def spellLanguage(self):
+ return self._spellLanguage
+
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't
@@ -52,49 +60,53 @@ class NWSpellEnchant():
"""
try:
import enchant
- if self.theBroker is not None:
+ if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker")
- del self.theBroker
+ del self._theBroker
- self.theBroker = enchant.Broker()
- self.theDict = self.theBroker.request_dict(theLang)
- self.spellLanguage = theLang
+ self._theBroker = enchant.Broker()
+ self._theDict = self._theBroker.request_dict(theLang)
+ self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
- self.theDict = FakeEnchant()
- self.spellLanguage = None
+ self._theDict = FakeEnchant()
+ self._spellLanguage = None
self._readProjectDictionary(projectDict)
- for pWord in self.projDict:
- self.theDict.add_to_session(pWord)
+ for pWord in self._projDict:
+ self._theDict.add_to_session(pWord)
return
+ ##
+ # Methods
+ ##
+
def checkWord(self, theWord):
"""Wrapper function for pyenchant.
"""
- return self.theDict.check(theWord)
+ return self._theDict.check(theWord)
def suggestWords(self, theWord):
"""Wrapper function for pyenchant.
"""
- return self.theDict.suggest(theWord)
+ return self._theDict.suggest(theWord)
def addWord(self, newWord):
"""Add a word to the project dictionary.
"""
- self.theDict.add_to_session(newWord)
+ self._theDict.add_to_session(newWord)
- if self.projectDict is not None and newWord not in self.projDict:
+ if self._projectDict is not None and newWord not in self._projDict:
newWord = newWord.strip()
try:
- with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
+ with open(self._projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
- self.projDict.add(newWord)
+ self._projDict.add(newWord)
except Exception:
- logger.error("Failed to add word to project word list %s", str(self.projectDict))
+ logger.error("Failed to add word to project word list %s", str(self._projectDict))
novelwriter.logException()
return False
return True
@@ -119,8 +131,8 @@ class NWSpellEnchant():
dictionary.
"""
try:
- spTag = self.theDict.tag
- spName = self.theDict.provider.name
+ spTag = self._theDict.tag
+ spName = self._theDict.provider.name
except Exception:
logger.error("Failed to extract information about the dictionary")
novelwriter.logException()
@@ -137,8 +149,8 @@ class NWSpellEnchant():
"""Read the content of the project dictionary, and add it to the
lookup lists.
"""
- self.projDict = set()
- self.projectDict = projectDict
+ self._projDict = set()
+ self._projectDict = projectDict
if projectDict is None:
return False
@@ -151,9 +163,9 @@ class NWSpellEnchant():
with open(projectDict, mode="r", encoding="utf-8") as wordsFile:
for theLine in wordsFile:
theLine = theLine.strip()
- if len(theLine) > 0 and theLine not in self.projDict:
- self.projDict.add(theLine)
- logger.debug("Project word list contains %d words", len(self.projDict))
+ if len(theLine) > 0 and theLine not in self._projDict:
+ self._projDict.add(theLine)
+ logger.debug("Project word list contains %d words", len(self._projDict))
except Exception:
logger.error("Failed to load project word list")
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index b6af70f3..7e9e4a74 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -109,7 +109,7 @@ class ToHtml(Tokenizer):
to theResult.
"""
if self.genMode == self.M_PREVIEW:
- htmlTags = { # HTML4 + CSS2
+ htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "",
self.FMT_B_E: "",
self.FMT_I_B: "",
@@ -118,7 +118,7 @@ class ToHtml(Tokenizer):
self.FMT_D_E: "",
}
else:
- htmlTags = { # HTML5
+ htmlTags = { # HTML5 (for export)
self.FMT_B_B: "",
self.FMT_B_E: "",
self.FMT_I_B: "",
@@ -176,17 +176,18 @@ class ToHtml(Tokenizer):
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
- aStyle.append("margin-left: %dpx;" % self.mainConf.tabWidth)
+ aStyle.append(f"margin-left: {self.mainConf.tabWidth:d}px;")
if tStyle & self.A_IND_R:
- aStyle.append("margin-right: %dpx;" % self.mainConf.tabWidth)
+ aStyle.append(f"margin-right: {self.mainConf.tabWidth:d}px;")
if len(aStyle) > 0:
- hStyle = " style='%s'" % (" ".join(aStyle))
+ stVals = " ".join(aStyle)
+ hStyle = f" style='{stVals}'"
else:
hStyle = ""
if self.linkHeaders:
- aNm = "" % tLine
+ aNm = f""
else:
aNm = ""
@@ -200,36 +201,36 @@ class ToHtml(Tokenizer):
parClass = ""
if len(thisPar) > 0:
tTemp = "
".join(thisPar)
- tmpResult.append("%s
\n" % (parClass, parStyle, tTemp.rstrip()))
+ tmpResult.append(f"{tTemp.rstrip()}
\n")
thisPar = []
parStyle = None
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("%s%s
\n" % (hStyle, aNm, tHead))
+ tmpResult.append(f"{aNm}{tHead}
\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s%s>\n" % (h2, hStyle, aNm, tHead, h2))
+ tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s%s>%s%s%s>\n" % (h1, h1Cl, hStyle, aNm, tHead, h1))
+ tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s%s>\n" % (h2, hStyle, aNm, tHead, h2))
+ tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s%s>\n" % (h3, hStyle, aNm, tHead, h3))
+ tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "
")
- tmpResult.append("<%s%s>%s%s%s>\n" % (h4, hStyle, aNm, tHead, h4))
+ tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
elif tType == self.T_SEP:
- tmpResult.append("%s
\n" % tText)
+ tmpResult.append(f"{tText}
\n")
elif tType == self.T_SKIP:
tmpResult.append("
\n")
@@ -249,7 +250,7 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
- tTemp = "%s
\n" % (hStyle, self._formatKeywords(tText))
+ tTemp = f"{self._formatKeywords(tText)}
\n"
tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
@@ -298,9 +299,9 @@ class ToHtml(Tokenizer):
"""Replace tabs with spaces in the html.
"""
htmlText = []
- eightSpace = spaceChar*nSpaces
+ tabSpace = spaceChar*nSpaces
for aLine in self.fullHTML:
- htmlText.append(aLine.replace("\t", eightSpace))
+ htmlText.append(aLine.replace("\t", tabSpace))
self.fullHTML = htmlText
return
@@ -315,75 +316,76 @@ class ToHtml(Tokenizer):
mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left"
- theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % (
+ theStyles.append("body {{font-family: '{0:s}'; font-size: {1:d}pt;}}".format(
self.textFont, self.textSize
))
theStyles.append((
- "p {"
- "text-align: %s; line-height: %d%%; "
- "margin-top: %.2fem; margin-bottom: %.2fem;"
- "}"
- ) % (
+ "p {{"
+ "text-align: {0}; line-height: {1:d}%; "
+ "margin-top: {2:.2f}em; margin-bottom: {3:.2f}em;"
+ "}}"
+ ).format(
textAlign,
round(100 * self.lineHeight),
mScale * self.marginText[0],
mScale * self.marginText[1],
))
theStyles.append((
- "h1 {"
+ "h1 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;"
- "}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
))
theStyles.append((
- "h2 {"
+ "h2 {{"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;"
- "}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
))
theStyles.append((
- "h3 {"
+ "h3 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;"
- "}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
))
theStyles.append((
- "h4 {"
+ "h4 {{"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;"
- "}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
))
theStyles.append((
- ".title {"
+ ".title {{"
"font-size: 2.5em; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;"
- "}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
))
theStyles.append((
- ".sep, .skip {"
+ ".sep, .skip {{"
"text-align: center; "
- "margin-top: %.2fem; "
- "margin-bottom: %.2fem;}"
- ) % (
+ "margin-top: {0:.2f}em; "
+ "margin-bottom: {1:.2f}em;"
+ "}}"
+ ).format(
mScale, mScale
))
@@ -421,31 +423,25 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
"""
- isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
+ isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
retText = ""
refTags = []
if theBits[0] in nwLabels.KEY_NAME:
- retText += "%s: " % nwLabels.KEY_NAME[theBits[0]]
+ retText += f"{nwLabels.KEY_NAME[theBits[0]]}: "
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
- retText += "%s" % (
- theBits[1], theBits[1]
- )
+ retText += f"{theBits[1]}"
else:
if self.genMode == self.M_PREVIEW:
for tTag in theBits[1:]:
- refTags.append("%s" % (
- theBits[0][1:], tTag, tTag
- ))
+ refTags.append(f"{tTag}")
retText += ", ".join(refTags)
else:
for tTag in theBits[1:]:
- refTags.append("%s" % (
- tTag, tTag
- ))
+ refTags.append(f"{tTag}")
retText += ", ".join(refTags)
return retText
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index c4b1be5e..a95c3f41 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -267,13 +267,14 @@ class Tokenizer():
else:
textAlign = self.A_PBB | self.A_CENTRE
- theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
+ locNotes = self._localLookup("Notes")
+ theTitle = f"{locNotes}: {theItem.itemName}"
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, textAlign
))
if self.keepMarkdown:
- self.theMarkdown.append("# %s\n\n" % theTitle)
+ self.theMarkdown.append(f"# {theTitle}\n\n")
return True
@@ -302,7 +303,7 @@ class Tokenizer():
errVal = self.tr("Document '{0}' is too big ({1} MB). Skipping.").format(
self.theItem.itemName, f"{docSize/1.0e6:.2f}"
)
- self.theText = "# %s\n\n%s\n\n" % (self.tr("ERROR"), errVal)
+ self.theText = "# {0}\n\n{1}\n\n".format(self.tr("ERROR"), errVal)
self.errData.append(errVal)
self.isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
@@ -318,7 +319,7 @@ class Tokenizer():
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
- repDict["<%s>" % aKey] = aVal
+ repDict[f"<{aKey}>"] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py
index 3ac0f58f..ff982868 100644
--- a/novelwriter/core/tomd.py
+++ b/novelwriter/core/tomd.py
@@ -100,33 +100,33 @@ class ToMarkdown(Tokenizer):
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
- tTemp = " \n".join(thisPar)
- tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
+ tTemp = (" \n".join(thisPar)).rstrip(" ")
+ tmpResult.append(f"{tTemp}\n\n")
thisPar = []
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("# %s\n\n" % tHead)
+ tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_UNNUM:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("## %s\n\n" % tHead)
+ tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("# %s\n\n" % tHead)
+ tmpResult.append(f"# {tHead}\n\n")
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("## %s\n\n" % tHead)
+ tmpResult.append(f"## {tHead}\n\n")
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("### %s\n\n" % tHead)
+ tmpResult.append(f"### {tHead}\n\n")
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n")
- tmpResult.append("#### %s\n\n" % tHead)
+ tmpResult.append(f"#### {tHead}\n\n")
elif tType == self.T_SEP:
tmpResult.append("%s\n\n" % tText)
@@ -141,10 +141,12 @@ class ToMarkdown(Tokenizer):
thisPar.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self.doSynopsis:
- tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
+ locName = self._localLookup("Synopsis")
+ tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_COMMENT and self.doComments:
- tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
+ locName = self._localLookup("Comment")
+ tmpResult.append(f"**{locName}:** {tText}\n\n")
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
@@ -189,7 +191,7 @@ class ToMarkdown(Tokenizer):
retText = ""
if theBits[0] in nwLabels.KEY_NAME:
- retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]]
+ retText += f"**{nwLabels.KEY_NAME[theBits[0]]}:** "
if len(theBits) > 1:
retText += ", ".join(theBits[1:])
diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py
index 3529855d..4db138e7 100644
--- a/novelwriter/core/toodt.py
+++ b/novelwriter/core/toodt.py
@@ -45,18 +45,35 @@ XML_NS = {
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
}
+MANI_NS = {
+ "manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
+}
+OFFICE_NS = {
+ "office": XML_NS["office"]
+}
+
+
+def _mkTag(nsName, tagName, nsMap=XML_NS):
+ """Assemble namespace and tag name.
+ """
+ theNS = nsMap.get(nsName, "")
+ if theNS:
+ return f"{{{theNS}}}{tagName}"
+ logger.warning("Missing xml namespace '%s'", nsName)
+ return tagName
+
# Mimetype and Version
X_MIME = "application/vnd.oasis.opendocument.text"
X_VERS = "1.2"
# Text Formatting Tags
-TAG_BR = "{%s}line-break" % XML_NS["text"]
-TAG_SPC = "{%s}s" % XML_NS["text"]
-TAG_NSPC = "{%s}c" % XML_NS["text"]
-TAG_TAB = "{%s}tab" % XML_NS["text"]
-TAG_SPAN = "{%s}span" % XML_NS["text"]
-TAG_STNM = "{%s}style-name" % XML_NS["text"]
+TAG_BR = _mkTag("text", "line-break")
+TAG_SPC = _mkTag("text", "s")
+TAG_NSPC = _mkTag("text", "c")
+TAG_TAB = _mkTag("text", "tab")
+TAG_SPAN = _mkTag("text", "span")
+TAG_STNM = _mkTag("text", "style-name")
# Formatting Codes
X_BLD = 0x01 # Bold format
@@ -313,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
- xMeta.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
+ xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -472,24 +489,22 @@ class ToOdt(Tokenizer):
def saveOpenDocText(self, savePath):
"""Save the data to an .odt file.
"""
- mMap = {"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"}
- mMani = "{%s}manifest" % mMap["manifest"]
- mVers = "{%s}version" % mMap["manifest"]
- mPath = "{%s}full-path" % mMap["manifest"]
- mType = "{%s}media-type" % mMap["manifest"]
- mFile = "{%s}file-entry" % mMap["manifest"]
+ mMani = _mkTag("manifest", "manifest", nsMap=MANI_NS)
+ mVers = _mkTag("manifest", "version", nsMap=MANI_NS)
+ mPath = _mkTag("manifest", "full-path", nsMap=MANI_NS)
+ mType = _mkTag("manifest", "media-type", nsMap=MANI_NS)
+ mFile = _mkTag("manifest", "file-entry", nsMap=MANI_NS)
- xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=mMap)
+ xMani = etree.Element(mMani, attrib={mVers: X_VERS}, nsmap=MANI_NS)
etree.SubElement(xMani, mFile, attrib={mPath: "/", mVers: X_VERS, mType: X_MIME})
etree.SubElement(xMani, mFile, attrib={mPath: "settings.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "content.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "meta.xml", mType: "text/xml"})
etree.SubElement(xMani, mFile, attrib={mPath: "styles.xml", mType: "text/xml"})
- sMap = {"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0"}
- oRoot = "{%s}document-settings" % sMap["office"]
- oSett = "{%s}settings" % sMap["office"]
- xSett = etree.Element(oRoot, nsmap=sMap)
+ oRoot = _mkTag("office", "document-settings", nsMap=OFFICE_NS)
+ oSett = _mkTag("office", "settings", nsMap=OFFICE_NS)
+ xSett = etree.Element(oRoot, nsmap=OFFICE_NS)
etree.SubElement(xSett, oSett)
with ZipFile(savePath, mode="w") as outFile:
@@ -520,16 +535,16 @@ class ToOdt(Tokenizer):
"""Apply formatting to synopsis lines.
"""
sSynop = self._localLookup("Synopsis")
- rTxt = "**%s:** %s" % (sSynop, tText)
- rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
+ rTxt = "**{0}:** {1}".format(sSynop, tText)
+ rFmt = "_B{0} b_ {1}".format(" "*len(sSynop), " "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
sComm = self._localLookup("Comment")
- rTxt = "**%s:** %s" % (sComm, tText)
- rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
+ rTxt = "**{0}:** {1}".format(sComm, tText)
+ rFmt = "_B{0} b_ {1}".format(" "*len(sComm), " "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
@@ -543,8 +558,8 @@ class ToOdt(Tokenizer):
rFmt = ""
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
- rTxt += "**%s:** " % tText
- rFmt += "_B%s b_ " % (" "*len(tText))
+ rTxt += "**{0}:** ".format(tText)
+ rFmt += "_B{0} b_ ".format(" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += theBits[1]
@@ -1468,16 +1483,4 @@ class XMLParagraph():
return
-
-# =============================================================================================== #
-# Local Functions
-# =============================================================================================== #
-
-def _mkTag(nsName, tagName):
- """Assemble namespace and tag name.
- """
- theNS = XML_NS.get(nsName, "")
- if theNS:
- return "{%s}%s" % (theNS, tagName)
- logger.warning("Missing xml namespace '%s'", nsName)
- return tagName
+# END Class XMLParagraph
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 5b6fe6d1..04b754aa 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -158,7 +158,7 @@ class NWTree():
continue
tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
- tocLine = "%-25s %-9s %-8s %s" % (
+ tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
os.path.join("content", tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
@@ -175,7 +175,7 @@ class NWTree():
outFile.write("Table of Contents\n")
outFile.write("=================\n")
outFile.write("\n")
- outFile.write("%-25s %-9s %-8s %s\n" % (
+ outFile.write("{0:<25s} {1:<9s} {2:<8s} {3:s}\n".format(
"File Name", "Class", "Layout", "Document Label"
))
outFile.write("-"*max(tocLen, 62) + "\n")
@@ -285,12 +285,12 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
- for i in range(nwConst.MAX_DEPTH + 1):
+ for _ in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tTree
else:
tHandle = tItem.itemParent
- tItem = self.__getitem__(tHandle)
+ tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
else:
@@ -341,7 +341,7 @@ class NWTree():
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
- logger.error("Item %s is not a file", tHandle)
+ logger.error("Item '%s' is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
return False
diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py
index 2aefc3f6..d9f89f3c 100644
--- a/novelwriter/dialogs/about.py
+++ b/novelwriter/dialogs/about.py
@@ -65,7 +65,7 @@ class GuiAbout(QDialog):
self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("novelWriter")
- self.lblVers = QLabel("v%s" % novelwriter.__version__)
+ self.lblVers = QLabel(f"v{novelwriter.__version__}")
self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x"))
self.leftBox = QVBoxLayout()
@@ -178,7 +178,7 @@ class GuiAbout(QDialog):
),
)
- aboutMsg += "%s
%s
" % (
+ aboutMsg += "{0}
{1}
".format(
self.tr("Translations"),
self._wrapTable([
("English", "Veronica Berglyd Olsen"),
@@ -193,7 +193,7 @@ class GuiAbout(QDialog):
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName and theTheme.themeAuthor != "N/A":
licURL = f"{theTheme.themeLicense}"
- aboutMsg += "%s
%s
" % (
+ aboutMsg += "{0}
{1}
".format(
self.tr("Theme: {0}").format(theTheme.themeName),
self._wrapTable([
(self.tr("Author"), theTheme.themeAuthor),
@@ -204,7 +204,7 @@ class GuiAbout(QDialog):
if theIcons.themeName:
licURL = f"{theIcons.themeLicense}"
- aboutMsg += "%s
%s
" % (
+ aboutMsg += "{0}
{1}
".format(
self.tr("Icons: {0}").format(theIcons.themeName),
self._wrapTable([
(self.tr("Author"), theIcons.themeAuthor),
@@ -215,7 +215,7 @@ class GuiAbout(QDialog):
if theTheme.syntaxName:
licURL = f"{theTheme.syntaxLicense}"
- aboutMsg += "%s
%s
" % (
+ aboutMsg += "{0}
{1}
".format(
self.tr("Syntax: {0}").format(theTheme.syntaxName),
self._wrapTable([
(self.tr("Author"), theTheme.syntaxAuthor),
@@ -258,7 +258,7 @@ class GuiAbout(QDialog):
theTable.append(
f"| {aLabel}: | {aValue} |
"
)
- return "" % "".join(theTable)
+ return "".format("".join(theTable))
def _setStyleSheet(self):
"""Set stylesheet for all browser tabs
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index e73e44fa..5b20c1e5 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -55,7 +55,7 @@ class GuiDocMerge(QDialog):
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Merge Documents"))
- self.headLabel = QLabel("%s" % self.tr("Documents to Merge"))
+ self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText
)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index ed98fdd1..8d6002d6 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -59,7 +59,7 @@ class GuiDocSplit(QDialog):
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document"))
- self.headLabel = QLabel("%s" % self.tr("Document Headers"))
+ self.headLabel = QLabel("{0}".format(self.tr("Document Headers")))
self.helpLabel = QHelpLabel(
self.tr("Select the maximum level to split into files."),
self.theParent.theTheme.helpText
@@ -174,7 +174,7 @@ class GuiDocSplit(QDialog):
msgYes = self.theParent.askQuestion(
self.tr("Split Document"),
- "%s
%s" % (
+ "{0}
{1}".format(
self.tr(
"The document will be split into {0} file(s) in a new folder. "
"The original document will remain intact.").format(nFiles),
@@ -196,10 +196,8 @@ class GuiDocSplit(QDialog):
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
- if srcItem.itemClass == nwItemClass.NOVEL:
- itemLayout = nwItemLayout.DOCUMENT
- else:
- itemLayout = nwItemLayout.NOTE
+ isNovel = srcItem.itemClass == nwItemClass.NOVEL
+ itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
@@ -209,9 +207,8 @@ class GuiDocSplit(QDialog):
newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus)
logger.verbose(
- "Creating new document %s with text from line %d to %d" % (
- nHandle, iStart+1, iEnd
- )
+ "Creating new document '%s' with text from line %d to %d",
+ nHandle, iStart+1, iEnd
)
theText = "\n".join(self.sourceText[iStart:iEnd])
@@ -273,7 +270,8 @@ class GuiDocSplit(QDialog):
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug(
- "Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel)
+ "Scanning document '%s' for headings level <= %d",
+ self.sourceItem, spLevel
)
self.sourceText = theText.splitlines()
diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py
index a49bcdca..784b8f69 100644
--- a/novelwriter/dialogs/itemeditor.py
+++ b/novelwriter/dialogs/itemeditor.py
@@ -57,7 +57,8 @@ class GuiItemEditor(QDialog):
self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None:
- self._doClose()
+ self.close()
+ return
self.setWindowTitle(self.tr("Item Settings"))
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index b23a8aaf..9db0ca15 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -719,7 +719,7 @@ class GuiDocEditor(QTextEdit):
), nwAlert.INFO)
theMode = False
- if self.spEnchant.spellLanguage is None:
+ if self.spEnchant.spellLanguage() is None:
theMode = False
self._spellCheck = theMode
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 1b32718f..ad25f5fe 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -784,7 +784,7 @@ class GuiMain(QMainWindow):
def passDocumentAction(self, theAction):
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
- its clid widgets have focus. If neither has focus, ignore the
+ its child widgets have focus. If neither has focus, ignore the
action.
"""
if self.docViewer.hasFocus():
@@ -836,13 +836,13 @@ class GuiMain(QMainWindow):
if tHandle is None:
logger.warning("No item selected")
- return
+ return False
tItem = self.theProject.projTree[tHandle]
if tItem is None:
- return
+ return False
if tItem.itemType not in nwLists.REG_TYPES:
- return
+ return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
@@ -853,7 +853,7 @@ class GuiMain(QMainWindow):
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
- return
+ return True
def rebuildTrees(self):
"""Rebuild the project tree.
@@ -938,7 +938,7 @@ class GuiMain(QMainWindow):
##
def showProjectLoadDialog(self):
- """Opens the projects dialog for selecting either existing
+ """Open the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached. Selecting to create a
new project is forwarded to the new project wizard.
@@ -1058,7 +1058,7 @@ class GuiMain(QMainWindow):
return
def showWritingStatsDialog(self):
- """Open the session log dialog.
+ """Open the session stats dialog.
"""
if not self.hasProject:
logger.error("No project open")
@@ -1092,17 +1092,17 @@ class GuiMain(QMainWindow):
if showNotes:
dlgAbout.showReleaseNotes()
- return
+ return True
def showAboutQtDialog(self):
"""Show the about dialog for Qt.
"""
msgBox = QMessageBox()
msgBox.aboutQt(self, "About Qt")
- return
+ return True
def showUpdatesDialog(self):
- """Show the updates dialog for novelWriter.
+ """Show the check for updates dialog.
"""
dlgUpdate = getGuiItem("GuiUpdates")
if dlgUpdate is None:
@@ -1117,11 +1117,11 @@ class GuiMain(QMainWindow):
return
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
- """Alert both the user and the logger at the same time. Message
- can be either a string or an array of strings.
+ """Alert both the user and the logger at the same time. The
+ message can be either a string or a list of strings.
"""
if isinstance(theMessage, list):
- theMessage = list(filter(None, theMessage))
+ theMessage = list(filter(None, theMessage)) # Strip empty strings
popMsg = "
".join(theMessage)
logMsg = theMessage
else:
@@ -1246,13 +1246,11 @@ class GuiMain(QMainWindow):
self.theProject.setLastViewed(None)
bPos = self.splitMain.sizes()
self.splitView.setVisible(False)
- vPos = [bPos[1], 0]
- self.splitDocs.setSizes(vPos)
+ self.splitDocs.setSizes([bPos[1], 0])
return not self.splitView.isVisible()
def toggleFocusMode(self):
- """Main GUI Focus Mode hides tree, view pane and optionally also
- statusbar and menu.
+ """Main GUI Focus Mode hides tree, view, statusbar and menu.
"""
if self.docEditor.docHandle() is None:
logger.error("No document open, so not activating Focus Mode")
@@ -1409,7 +1407,7 @@ class GuiMain(QMainWindow):
return True
def _autoSaveProject(self):
- """Triggered by the auto-save project timer to save the project.
+ """Triggered by the autosave project timer to save the project.
"""
doSave = self.hasProject
doSave &= self.theProject.projChanged
@@ -1422,7 +1420,7 @@ class GuiMain(QMainWindow):
return
def _autoSaveDocument(self):
- """Triggered by the auto-save document timer to save the
+ """Triggered by the autosave document timer to save the
document.
"""
if self.hasProject and self.docEditor.docChanged():
@@ -1511,7 +1509,7 @@ class GuiMain(QMainWindow):
@pyqtSlot()
def _timeTick(self):
- """Triggered on every tick of the timer.
+ """Triggered on every tick of the main timer.
"""
if not self.hasProject:
return
diff --git a/tests/reference/guiItemEditor_Dialog_nwProject.nwx b/tests/reference/guiItemEditor_Dialog_nwProject.nwx
deleted file mode 100644
index 17f65bcd..00000000
--- a/tests/reference/guiItemEditor_Dialog_nwProject.nwx
+++ /dev/null
@@ -1,115 +0,0 @@
-
-
-
- New Project
-
- 2
- 1
- 0
-
-
- True
- None
- False
- None
- True
- 0e17daca5f3e1
- None
- 6
- 6
- 0
-
-
- %title%
- %title%
- %title%
- * * *
-
-
-
- New
- Note
- Draft
- Finished
-
-
- New
- Minor
- Major
- Main
-
-
-
- -
- Novel
- ROOT
- NOVEL
- New
- True
-
- -
- Title Page
- FILE
- NOVEL
- New
- True
- DOCUMENT
- 11
- 2
- 0
- 0
-
- -
- New Chapter
- FOLDER
- NOVEL
- New
- True
-
- -
- New Chapter
- FILE
- NOVEL
- New
- True
- DOCUMENT
- 11
- 2
- 0
- 0
-
- -
- Just a Page
- FILE
- NOVEL
- Note
- False
- DOCUMENT
- 9
- 2
- 0
- 0
-
- -
- Plot
- ROOT
- PLOT
- New
- False
-
- -
- Characters
- ROOT
- CHARACTER
- New
- False
-
- -
- World
- ROOT
- WORLD
- New
- False
-
-
-
diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py
index 25527513..70fe4c72 100644
--- a/tests/test_base/test_base_common.py
+++ b/tests/test_base/test_base_common.py
@@ -31,8 +31,8 @@ from tools import writeFile
from novelwriter.guimain import GuiMain
from novelwriter.common import (
- checkString, checkInt, checkBool, checkHandle, isHandle, isTitleTag,
- isItemClass, isItemType, isItemLayout, hexToInt, formatInt,
+ checkString, checkInt, checkFloat, checkBool, checkHandle, isHandle,
+ isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, formatInt,
formatTimeStamp, formatTime, parseTimeStamp, splitVersionNumber,
transferCase, fuzzyTime, numberToRoman, jsonEncode, readTextFile,
makeFileNameSafe, sha256sum, getGuiItem, NWConfigParser
@@ -68,6 +68,20 @@ def testBaseCommon_CheckInt():
# END Test testBaseCommon_CheckInt
+@pytest.mark.base
+def testBaseCommon_CheckFloat():
+ """Test the checkFloat function.
+ """
+ assert checkFloat(None, 3.0, True) is None
+ assert checkFloat("None", 3.0, True) is None
+ assert checkFloat(None, 3.0, False) == 3.0
+ assert checkFloat(1, 3.0, False) == 1.0
+ assert checkFloat(1.0, 3.0, False) == 1.0
+ assert checkFloat(True, 3.0, False) == 1.0
+
+# END Test testBaseCommon_CheckInt
+
+
@pytest.mark.base
def testBaseCommon_CheckBool():
"""Test the checkBool function.
diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py
index 41e0933d..7ef1b0ec 100644
--- a/tests/test_core/test_core_options.py
+++ b/tests/test_core/test_core_options.py
@@ -73,7 +73,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
assert theOpts.loadSettings()
# Check that unwanted items have been removed
- assert theOpts.theState == {
+ assert theOpts._theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -88,7 +88,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, tmpDir):
# Load again to check we get the values back
assert theOpts.loadSettings()
- assert theOpts.theState == {
+ assert theOpts._theState == {
"GuiBuildNovel": {
"winWidth": 1000,
"winHeight": 700,
@@ -129,7 +129,7 @@ def testCoreOptions_SetGet(mockGUI):
assert theOpts.getValue("GuiBuildNovel", "mockItem", None) is None
# Get type-specific
- assert theOpts.getString("GuiBuildNovel", "winWidth", None) == "100"
+ assert theOpts.getString("GuiBuildNovel", "winWidth", None) is None
assert theOpts.getString("GuiBuildNovel", "mockItem", None) is None
assert theOpts.getInt("GuiBuildNovel", "winWidth", None) == 100
assert theOpts.getInt("GuiBuildNovel", "textFont", None) is None
diff --git a/tests/test_core/test_core_spell.py b/tests/test_core/test_core_spell.py
index db8937df..7d0df7cc 100644
--- a/tests/test_core/test_core_spell.py
+++ b/tests/test_core/test_core_spell.py
@@ -61,7 +61,7 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
assert spChk._readProjectDictionary(None) is False
assert spChk._readProjectDictionary(wList) is True
- assert spChk.projectDict == wList
+ assert spChk._projectDict == wList
# Cannot write to file
with monkeypatch.context() as mp:
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index d0450147..d569d983 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -27,59 +27,6 @@ from tools import readFile
from novelwriter.core import NWProject, NWIndex, ToHtml
-@pytest.mark.core
-def testCoreToHtml_Format(mockGUI):
- """Test all the formatters for the ToHtml class.
- """
- theProject = NWProject(mockGUI)
- mockGUI.theIndex = NWIndex(theProject)
- theHtml = ToHtml(theProject)
-
- # Export Mode
- # ===========
-
- assert theHtml._formatSynopsis("synopsis text") == (
- "Synopsis: synopsis text
\n"
- )
- assert theHtml._formatComments("comment text") == (
- "\n"
- )
-
- assert theHtml._formatKeywords("") == ""
- assert theHtml._formatKeywords("tag: Jane") == (
- "Tag: Jane"
- )
- assert theHtml._formatKeywords("char: Bod, Jane") == (
- "Characters: "
- "Bod, "
- "Jane"
- )
-
- # Preview Mode
- # ============
-
- theHtml.setPreview(True, True)
-
- assert theHtml._formatSynopsis("synopsis text") == (
- "\n"
- )
- assert theHtml._formatComments("comment text") == (
- "\n"
- )
-
- assert theHtml._formatKeywords("") == ""
- assert theHtml._formatKeywords("tag: Jane") == (
- "Tag: Jane"
- )
- assert theHtml._formatKeywords("char: Bod, Jane") == (
- "Characters: "
- "Bod, "
- "Jane"
- )
-
-# END Test testCoreToHtml_Format
-
-
@pytest.mark.core
def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class.
@@ -433,7 +380,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
@pytest.mark.core
def testCoreToHtml_Complex(mockGUI, fncDir):
- """Test the ave method of the ToHtml class.
+ """Test the save method of the ToHtml class.
"""
theProject = NWProject(mockGUI)
theHtml = ToHtml(theProject)
@@ -524,7 +471,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
theHtml.saveHTML5(saveFile)
assert readFile(saveFile) == htmlDoc
-# END Test testCoreToHtml_Save
+# END Test testCoreToHtml_Complex
@pytest.mark.core
@@ -589,3 +536,56 @@ def testCoreToHtml_Methods(mockGUI):
assert theHtml.getStyleSheet() == []
# END Test testCoreToHtml_Methods
+
+
+@pytest.mark.core
+def testCoreToHtml_Format(mockGUI):
+ """Test all the formatters for the ToHtml class.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theHtml = ToHtml(theProject)
+
+ # Export Mode
+ # ===========
+
+ assert theHtml._formatSynopsis("synopsis text") == (
+ "Synopsis: synopsis text
\n"
+ )
+ assert theHtml._formatComments("comment text") == (
+ "\n"
+ )
+
+ assert theHtml._formatKeywords("") == ""
+ assert theHtml._formatKeywords("tag: Jane") == (
+ "Tag: Jane"
+ )
+ assert theHtml._formatKeywords("char: Bod, Jane") == (
+ "Characters: "
+ "Bod, "
+ "Jane"
+ )
+
+ # Preview Mode
+ # ============
+
+ theHtml.setPreview(True, True)
+
+ assert theHtml._formatSynopsis("synopsis text") == (
+ "\n"
+ )
+ assert theHtml._formatComments("comment text") == (
+ "\n"
+ )
+
+ assert theHtml._formatKeywords("") == ""
+ assert theHtml._formatKeywords("tag: Jane") == (
+ "Tag: Jane"
+ )
+ assert theHtml._formatKeywords("char: Bod, Jane") == (
+ "Characters: "
+ "Bod, "
+ "Jane"
+ )
+
+# END Test testCoreToHtml_Format
diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py
new file mode 100644
index 00000000..267cce92
--- /dev/null
+++ b/tests/test_core/test_core_tomd.py
@@ -0,0 +1,277 @@
+"""
+novelWriter – ToMd Class Tester
+===============================
+
+This file is a part of novelWriter
+Copyright 2018–2021, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import os
+import pytest
+
+from tools import readFile
+
+from novelwriter.core import NWProject, NWIndex, ToMarkdown
+
+
+@pytest.mark.core
+def testCoreToMarkdown_ConvertFormat(mockGUI):
+ """Test the tokenizer and converter chain using the ToMarkdown class.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theMD = ToMarkdown(theProject)
+
+ # Headers
+ # =======
+
+ theMD.isNovel = True
+ theMD.isNote = False
+ theMD.isFirst = True
+
+ # Header 1
+ theMD.theText = "# Partition\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "# Partition\n\n"
+
+ # Header 2
+ theMD.theText = "## Chapter Title\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "## Chapter Title\n\n"
+
+ # Header 3
+ theMD.theText = "### Scene Title\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "### Scene Title\n\n"
+
+ # Header 4
+ theMD.theText = "#### Section Title\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "#### Section Title\n\n"
+
+ # Title
+ theMD.theText = "#! Title\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "# Title\n\n"
+
+ # Unnumbered
+ theMD.theText = "##! Prologue\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "## Prologue\n\n"
+
+ # Paragraphs
+ # ==========
+
+ # Text for GitHub Markdown
+ theMD.setGitHubMarkdown()
+ theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == (
+ "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
+ )
+
+ # Text for Standard Markdown
+ theMD.setStandardMarkdown()
+ theMD.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == (
+ "Some **nested bold and _italic_ and strikethrough text** here\n\n"
+ )
+
+ # Text w/Hard Break
+ theMD.theText = "Line one \nLine two \nLine three\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "Line one \nLine two \nLine three\n\n"
+
+ # Synopsis
+ theMD.theText = "%synopsis: The synopsis ...\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == ""
+
+ theMD.setSynopsis(True)
+ theMD.theText = "%synopsis: The synopsis ...\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "**Synopsis:** The synopsis ...\n\n"
+
+ # Comment
+ theMD.theText = "% A comment ...\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == ""
+
+ theMD.setComments(True)
+ theMD.theText = "% A comment ...\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "**Comment:** A comment ...\n\n"
+
+ # Keywords
+ theMD.theText = "@char: Bod, Jane\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == ""
+
+ theMD.setKeywords(True)
+ theMD.theText = "@char: Bod, Jane\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == "**Characters:** Bod, Jane\n\n"
+
+ # Multiple Keywords
+ theMD.setKeywords(True)
+ theMD.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == (
+ "## Chapter\n\n"
+ "**Point of View:** Bod \n"
+ "**Plot:** Main \n"
+ "**Locations:** Europe\n\n"
+ )
+
+# END Test testCoreToMarkdown_ConvertFormat
+
+
+@pytest.mark.core
+def testCoreToMarkdown_ConvertDirect(mockGUI):
+ """Test the converter directly using the ToMarkdown class.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theMD = ToMarkdown(theProject)
+
+ theMD.isNovel = True
+ theMD.isNote = False
+
+ # Special Titles
+ # ==============
+
+ # Title
+ theMD.theTokens = [
+ (theMD.T_TITLE, 1, "A Title", None, theMD.A_PBB | theMD.A_CENTRE),
+ (theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
+ ]
+ theMD.doConvert()
+ assert theMD.theResult == "# A Title\n\n"
+
+ # Unnumbered
+ theMD.theTokens = [
+ (theMD.T_UNNUM, 1, "Prologue", None, theMD.A_PBB),
+ (theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
+ ]
+ theMD.doConvert()
+ assert theMD.theResult == "## Prologue\n\n"
+
+ # Separators
+ # ==========
+
+ # Separator
+ theMD.theTokens = [
+ (theMD.T_SEP, 1, "* * *", None, theMD.A_CENTRE),
+ (theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
+ ]
+ theMD.doConvert()
+ assert theMD.theResult == "* * *\n\n"
+
+ # Skip
+ theMD.theTokens = [
+ (theMD.T_SKIP, 1, "", None, theMD.A_NONE),
+ (theMD.T_EMPTY, 1, "", None, theMD.A_NONE),
+ ]
+ theMD.doConvert()
+ assert theMD.theResult == "\n\n\n"
+
+# END Test testCoreToMarkdown_ConvertDirect
+
+
+@pytest.mark.core
+def testCoreToMarkdown_Complex(mockGUI, fncDir):
+ """Test the save method of the ToMarkdown class.
+ """
+ theProject = NWProject(mockGUI)
+ theMD = ToMarkdown(theProject)
+ theMD.isNovel = True
+
+ # Build Project
+ # =============
+
+ docText = [
+ "# My Novel\n**By Jane Doh**\n",
+ "## Chapter 1\n\nThe text of chapter one.\n",
+ "### Scene 1\n\nThe text of scene one.\n",
+ "#### A Section\n\nMore text in scene one.\n",
+ "## Chapter 2\n\nThe text of chapter two.\n",
+ "### Scene 2\n\nThe text of scene two.\n",
+ "#### A Section\n\n\tMore text in scene two.\n",
+ ]
+ resText = [
+ "# My Novel\n\n**By Jane Doh**\n\n",
+ "## Chapter 1\n\nThe text of chapter one.\n\n",
+ "### Scene 1\n\nThe text of scene one.\n\n",
+ "#### A Section\n\nMore text in scene one.\n\n",
+ "## Chapter 2\n\nThe text of chapter two.\n\n",
+ "### Scene 2\n\nThe text of scene two.\n\n",
+ "#### A Section\n\n\tMore text in scene two.\n\n",
+ ]
+
+ for i in range(len(docText)):
+ theMD.theText = docText[i]
+ theMD.doPreProcessing()
+ theMD.tokenizeText()
+ theMD.doConvert()
+ assert theMD.theResult == resText[i]
+
+ assert theMD.fullMD == resText
+ assert theMD.getFullResultSize() == len("".join(resText))
+
+ theMD.replaceTabs(nSpaces=4, spaceChar=" ")
+ resText[6] = "#### A Section\n\n More text in scene two.\n\n"
+
+ # Check File
+ # ==========
+
+ saveFile = os.path.join(fncDir, "outFile.md")
+ theMD.saveMarkdown(saveFile)
+ assert readFile(saveFile) == "".join(resText)
+
+# END Test testCoreToHtml_Complex
+
+
+@pytest.mark.core
+def testCoreToMarkdown_Format(mockGUI):
+ """Test all the formatters for the ToMarkdown class.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theMD = ToMarkdown(theProject)
+
+ assert theMD._formatKeywords("", theMD.A_NONE) == ""
+ assert theMD._formatKeywords("tag: Jane", theMD.A_NONE) == "**Tag:** Jane\n\n"
+ assert theMD._formatKeywords("tag: Jane, John", theMD.A_NONE) == "**Tag:** Jane, John\n\n"
+ assert theMD._formatKeywords("tag: Jane", theMD.A_Z_BTMMRG) == "**Tag:** Jane \n"
+
+# END Test testCoreToMarkdown_Format
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index ca3c0d8a..403309d1 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -559,6 +559,62 @@ def testCoreToOdt_Convert(mockGUI):
# END Test testCoreToOdt_Convert
+@pytest.mark.core
+def testCoreToOdt_ConvertDirect(mockGUI):
+ """Test the converter directly using the ToOdt class to reach some
+ otherwise hard to reach conditions.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theDoc = ToOdt(theProject, isFlat=True)
+
+ theDoc.isNovel = True
+
+ # Justified
+ theDoc = ToOdt(theProject, isFlat=True)
+ theDoc.theTokens = [
+ (theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_JUSTIFY),
+ (theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
+ ]
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert (
+ ''
+ ''
+ ''
+ ) in xmlToText(theDoc._xAuto)
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'This is a paragraph'
+ ''
+ )
+
+ # Page Break After
+ theDoc = ToOdt(theProject, isFlat=True)
+ theDoc.theTokens = [
+ (theDoc.T_TEXT, 1, "This is a paragraph", [], theDoc.A_PBA),
+ (theDoc.T_EMPTY, 1, "", None, theDoc.A_NONE),
+ ]
+ theDoc.initDocument()
+ theDoc.doConvert()
+ theDoc.closeDocument()
+ assert (
+ ''
+ ''
+ ''
+ ) in xmlToText(theDoc._xAuto)
+ assert xmlToText(theDoc._xText) == (
+ ''
+ 'This is a paragraph'
+ ''
+ )
+
+# END Test testCoreToOdt_ConvertDirect
+
+
@pytest.mark.core
def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
"""Test the document save functions.
@@ -676,6 +732,36 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
# END Test testCoreToOdt_SaveFull
+@pytest.mark.core
+def testCoreToOdt_Format(mockGUI):
+ """Test the formatters for the ToOdt class.
+ """
+ theProject = NWProject(mockGUI)
+ mockGUI.theIndex = NWIndex(theProject)
+ theDoc = ToOdt(theProject, isFlat=True)
+
+ assert theDoc._formatSynopsis("synopsis text") == (
+ "**Synopsis:** synopsis text",
+ "_B b_ "
+ )
+ assert theDoc._formatComments("comment text") == (
+ "**Comment:** comment text",
+ "_B b_ "
+ )
+
+ assert theDoc._formatKeywords("") == ""
+ assert theDoc._formatKeywords("tag: Jane") == (
+ "**Tag:** Jane",
+ "_B b_ "
+ )
+ assert theDoc._formatKeywords("char: Bod, Jane") == (
+ "**Characters:** Bod, Jane",
+ "_B b_ "
+ )
+
+# END Test testCoreToOdt_Format
+
+
@pytest.mark.core
def testCoreToOdt_ODTParagraphStyle():
"""Test the ODTParagraphStyle class.
diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py
index 76f66984..2ce05223 100644
--- a/tests/test_dialogs/test_dlg_about.py
+++ b/tests/test_dialogs/test_dlg_about.py
@@ -27,23 +27,22 @@ from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.dialogs import GuiAbout
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
-
@pytest.mark.gui
-def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI):
- """Test the full about dialogs.
+def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
+ """Test the novelWriter about dialogs.
"""
- # NW About
- monkeypatch.setattr(GuiAbout, "exec_", lambda *a: None)
- nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ # NW About
+ nwGUI.theTheme.themeName = "A Theme"
+ nwGUI.theTheme.themeAuthor = "An Author"
+ assert nwGUI.showAboutNWDialog(showNotes=True) is True
+
+ qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
msgAbout = getGuiItem("GuiAbout")
assert isinstance(msgAbout, GuiAbout)
- msgAbout.show()
assert msgAbout.pageAbout.document().characterCount() > 100
assert msgAbout.pageNotes.document().characterCount() > 100
@@ -59,12 +58,29 @@ def testDlgAbout_Dialog(qtbot, monkeypatch, nwGUI):
msgAbout.showReleaseNotes()
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
-
- # Qt About
- monkeypatch.setattr(QMessageBox, "aboutQt", lambda *a, **k: None)
- nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
-
- # qtbot.stopForInteraction()
msgAbout._doClose()
-# END Test testDlgAbout_Dialog
+ # Open Again from Menu
+ nwGUI.mainMenu.aAboutNW.activate(QAction.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiAbout") is not None, timeout=1000)
+ msgAbout = getGuiItem("GuiAbout")
+ assert msgAbout is not None
+ msgAbout._doClose()
+
+# END Test testDlgAbout_NWDialog
+
+
+@pytest.mark.gui
+def testDlgAbout_QtDialog(monkeypatch, nwGUI):
+ """Test the Qt about dialogs.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "aboutQt", lambda *a, **k: None)
+
+ # Open About
+ # All it can do is check aigainst a crash
+ assert nwGUI.showAboutQtDialog() is True
+ nwGUI.mainMenu.aAboutQt.activate(QAction.Trigger)
+
+# END Test testDlgAbout_QtDialog
diff --git a/tests/test_dialogs/test_dlg_merge.py b/tests/test_dialogs/test_dlg_docmerge.py
similarity index 98%
rename from tests/test_dialogs/test_dlg_merge.py
rename to tests/test_dialogs/test_dlg_docmerge.py
index 17f566e9..f51aee3e 100644
--- a/tests/test_dialogs/test_dlg_merge.py
+++ b/tests/test_dialogs/test_dlg_docmerge.py
@@ -31,10 +31,6 @@ from novelwriter.dialogs import GuiDocMerge, GuiItemEditor
from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.tree import NWTree
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
-
@pytest.mark.gui
def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -96,7 +92,7 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwMerge = getGuiItem("GuiDocMerge")
assert isinstance(nwMerge, GuiDocMerge)
nwMerge.show()
- qtbot.wait(stepDelay)
+ qtbot.wait(50)
# Populate List
# =============
diff --git a/tests/test_dialogs/test_dlg_split.py b/tests/test_dialogs/test_dlg_docsplit.py
similarity index 99%
rename from tests/test_dialogs/test_dlg_split.py
rename to tests/test_dialogs/test_dlg_docsplit.py
index 32a9d138..c5bc9f3e 100644
--- a/tests/test_dialogs/test_dlg_split.py
+++ b/tests/test_dialogs/test_dlg_docsplit.py
@@ -32,10 +32,6 @@ from novelwriter.enum import nwItemType, nwWidget
from novelwriter.core.document import NWDoc
from novelwriter.core.tree import NWTree
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
-
@pytest.mark.gui
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
@@ -103,7 +99,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit = getGuiItem("GuiDocSplit")
assert isinstance(nwSplit, GuiDocSplit)
nwSplit.show()
- qtbot.wait(stepDelay)
+ qtbot.wait(50)
# Populate List
# =============
@@ -253,7 +249,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwSplit.sourceItem = None
assert nwSplit._doSplit() is False
- # Close up
+ # Close
nwSplit._doClose()
# qtbot.stopForInteraction()
diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py
index 5c6d2277..d921238d 100644
--- a/tests/test_dialogs/test_dlg_itemeditor.py
+++ b/tests/test_dialogs/test_dlg_itemeditor.py
@@ -20,93 +20,213 @@ along with this program. If not, see .
"""
import pytest
-import os
-from shutil import copyfile
-from tools import cmpFiles, getGuiItem
+from tools import getGuiItem
-from PyQt5.QtWidgets import QAction, QMessageBox
+from PyQt5.QtWidgets import QAction, QDialog, QMessageBox
from novelwriter.gui import GuiProjectTree
-from novelwriter.enum import nwItemLayout
+from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.dialogs import GuiItemEditor
-
-keyDelay = 2
-typeDelay = 1
-stepDelay = 20
+from novelwriter.core.tree import NWTree
@pytest.mark.gui
-def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
- """Test the full item editor dialog.
+def testDlgItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncProj):
+ """Test launching the item editor dialog from GuiMain.
"""
- projFile = os.path.join(fncProj, "nwProject.nwx")
- testFile = os.path.join(outDir, "guiItemEditor_Dialog_nwProject.nwx")
- compFile = os.path.join(refDir, "guiItemEditor_Dialog_nwProject.nwx")
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ # Block Dialog exec_
+ monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
+
+ # Open Editor wo/Project
+ assert nwGUI.editItem() is False
+
+ # Create and Open Project
+ nwGUI.theProject.projTree.setSeed(42)
+ assert nwGUI.newProject({"projPath": fncProj})
+
+ # No Selection
+ nwGUI.treeView.clearSelection()
+ assert nwGUI.editItem() is False
+
+ # Force opening from editor
+ assert nwGUI.openDocument("0e17daca5f3e1")
+ nwGUI.isFocusMode = True
+
+ # Block Tree Lookup
+ with monkeypatch.context() as mp:
+ mp.setattr(NWTree, "__getitem__", lambda *a: None)
+ assert nwGUI.editItem() is False
+
+ # Invalid Type
+ nwGUI.theProject.projTree["0e17daca5f3e1"].itemType = nwItemType.NO_TYPE
+ assert nwGUI.editItem() is False
+ nwGUI.theProject.projTree["0e17daca5f3e1"].itemType = nwItemType.FILE
+
+ # Open Properly
+ assert nwGUI.editItem() is True
+ qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
+ itemEdit = getGuiItem("GuiItemEditor")
+ assert itemEdit is not None
+ itemEdit.close()
+
+ # Open Via Menu
+ with monkeypatch.context() as mp:
+ mp.setattr(GuiItemEditor, "result", lambda *a: QDialog.Accepted)
+ nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
+ qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
+ itemEdit = getGuiItem("GuiItemEditor")
+ assert itemEdit is not None
+ itemEdit.close()
+
+ nwGUI.isFocusMode = False
+
+# END Test testDlgItemEditor_Dialog
+
+
+@pytest.mark.gui
+def testDlgItemEditor_Novel(qtbot, monkeypatch, nwGUI, fncProj):
+ """Test the item editor dialog for a novel document.
+ """
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
- # Create new, save, open project
+ # Create Project and Open Document
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj})
assert nwGUI.openDocument("0e17daca5f3e1")
- assert nwGUI.treeView.setSelectedHandle("0e17daca5f3e1", doScroll=True)
- monkeypatch.setattr(GuiItemEditor, "exec_", lambda *a: None)
- nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
+ # Check that an invalid handle is managed
+ itemEdit = GuiItemEditor(nwGUI, "whatever")
+ itemEdit.show()
+ itemEdit._doClose()
- itemEdit = getGuiItem("GuiItemEditor")
- assert isinstance(itemEdit, GuiItemEditor)
+ # Edit a Document
+ itemEdit = GuiItemEditor(nwGUI, "0e17daca5f3e1")
itemEdit.show()
- qtbot.addWidget(itemEdit)
-
+ # Check Existing Settings
assert itemEdit.editName.text() == "New Scene"
assert itemEdit.editStatus.currentData() == "New"
assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
+ assert itemEdit.editExport.isChecked() is True
- for c in "Just a Page":
- qtbot.keyClick(itemEdit.editName, c, delay=typeDelay)
+ # Change Settings
+ layoutIdx = itemEdit.editLayout.findData(nwItemLayout.NOTE)
+ itemEdit.editName.setText("Great Scene")
itemEdit.editStatus.setCurrentIndex(1)
- layoutIdx = itemEdit.editLayout.findData(nwItemLayout.DOCUMENT)
itemEdit.editLayout.setCurrentIndex(layoutIdx)
-
itemEdit.editExport.setChecked(False)
- assert not itemEdit.editExport.isChecked()
+
+ # Check New Settings
itemEdit._doSave()
+ assert itemEdit.theItem.itemName == "Great Scene"
+ assert itemEdit.theItem.itemStatus == "Note"
+ assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
+ assert itemEdit.theItem.isExported is False
- nwGUI.mainMenu.aEditItem.activate(QAction.Trigger)
- qtbot.waitUntil(lambda: getGuiItem("GuiItemEditor") is not None, timeout=1000)
-
- itemEdit = getGuiItem("GuiItemEditor")
- assert isinstance(itemEdit, GuiItemEditor)
- itemEdit.show()
-
- qtbot.addWidget(itemEdit)
- assert itemEdit.editName.text() == "Just a Page"
- assert itemEdit.editStatus.currentData() == "Note"
- assert itemEdit.editLayout.currentData() == nwItemLayout.DOCUMENT
- itemEdit._doClose()
-
- # Check that the header is updated
+ # Check that the editor header is updated
nwGUI.docEditor.updateDocInfo("0e17daca5f3e1")
- assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Just a Page"
- assert not nwGUI.docEditor.setCursorLine("where?")
- assert nwGUI.docEditor.setCursorLine(2)
- qtbot.wait(stepDelay)
- assert nwGUI.docEditor.getCursorPosition() == 15
-
- qtbot.wait(stepDelay)
- assert nwGUI.saveProject()
- qtbot.wait(stepDelay)
-
- # Check the files
- copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
+ assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Great Scene"
+ itemEdit.close()
+ del itemEdit
# qtbot.stopForInteraction()
# END Test testDlgItemEditor_Dialog
+
+
+@pytest.mark.gui
+def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj):
+ """Test the item editor dialog for a project note.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
+
+ # Create Project and Open Document
+ nwGUI.theProject.projTree.setSeed(42)
+ assert nwGUI.newProject({"projPath": fncProj})
+
+ # Create Note
+ nwGUI.treeView.clearSelection()
+ nwGUI.treeView._getTreeItem("71ee45a3c0db9").setSelected(True)
+ nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
+
+ # Open Note
+ assert nwGUI.openDocument("1a6562590ef19")
+
+ # Edit a Document
+ itemEdit = GuiItemEditor(nwGUI, "1a6562590ef19")
+ itemEdit.show()
+
+ # Check Existing Settings
+ assert itemEdit.editName.text() == "New File"
+ assert itemEdit.editStatus.currentData() == "New"
+ assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
+ assert itemEdit.editExport.isChecked() is True
+
+ # Change Settings
+ itemEdit.editName.setText("New Character")
+ itemEdit.editStatus.setCurrentIndex(1)
+ itemEdit.editExport.setChecked(False)
+
+ # Check New Settings
+ itemEdit._doSave()
+ assert itemEdit.theItem.itemName == "New Character"
+ assert itemEdit.theItem.itemStatus == "Minor"
+ assert itemEdit.theItem.itemLayout == nwItemLayout.NOTE
+ assert itemEdit.theItem.isExported is False
+
+ itemEdit.close()
+ del itemEdit
+ # qtbot.stopForInteraction()
+
+# END Test testDlgItemEditor_Note
+
+
+@pytest.mark.gui
+def testDlgItemEditor_Folder(qtbot, monkeypatch, nwGUI, fncProj):
+ """Test the item editor dialog for a folder.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
+
+ # Create Project and Open Document
+ nwGUI.theProject.projTree.setSeed(42)
+ assert nwGUI.newProject({"projPath": fncProj})
+
+ # Edit a Folder
+ itemEdit = GuiItemEditor(nwGUI, "31489056e0916")
+ itemEdit.show()
+
+ # Check Existing Settings
+ assert itemEdit.editName.text() == "New Chapter"
+ assert itemEdit.editStatus.currentData() == "New"
+ assert itemEdit.editLayout.currentData() == nwItemLayout.NO_LAYOUT
+ assert itemEdit.editExport.isChecked() is False
+
+ assert itemEdit.editLayout.isEnabled() is False
+ assert itemEdit.editExport.isEnabled() is False
+
+ # Change Settings
+ itemEdit.editName.setText("Chapter One")
+ itemEdit.editStatus.setCurrentIndex(1)
+
+ # Check New Settings
+ itemEdit._doSave()
+ assert itemEdit.theItem.itemName == "Chapter One"
+ assert itemEdit.theItem.itemStatus == "Note"
+ assert itemEdit.theItem.itemLayout == nwItemLayout.NO_LAYOUT
+ assert itemEdit.theItem.isExported is False
+
+ itemEdit.close()
+ del itemEdit
+ # qtbot.stopForInteraction()
+
+# END Test testDlgItemEditor_Folder