From 27ee82aad0abe157a515997ee12104b0676f5e31 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 17 Sep 2020 14:03:51 +0200 Subject: [PATCH 1/7] Improve the way the project tree is managed and elements deleted. --- nw/common.py | 12 ++++++++++++ nw/core/tree.py | 41 ++++++++++++++++++++--------------------- nw/gui/projtree.py | 10 +++++----- tests/test_common.py | 11 ++++++++++- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/nw/common.py b/nw/common.py index 2d82290b..febac7cc 100644 --- a/nw/common.py +++ b/nw/common.py @@ -82,6 +82,18 @@ def checkBool(checkValue, defaultValue, allowNone=False): return defaultValue return defaultValue +def checkHandle(checkValue, defaultValue, allowNone=False): + """Check if a value is a handle. + """ + if allowNone: + if checkValue is None: + return None + if checkValue == "None": + return None + if isHandle(checkValue): + return str(checkValue) + return defaultValue + def isHandle(theString): """Check if a string is a valid novelWriter handle. Note: This is case sensitive. Must be lower case! diff --git a/nw/core/tree.py b/nw/core/tree.py index 0fcf2061..e9e39820 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -34,7 +34,7 @@ from hashlib import sha256 from time import time from nw.core.item import NWItem -from nw.common import checkString +from nw.common import checkHandle from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst logger = logging.getLogger(__name__) @@ -49,7 +49,6 @@ class NWTree(): self._treeOrder = [] # The order of the tree items on the tree view self._treeRoots = [] # The root items of the tree self._trashRoot = None # The handle of the trash root folder - self._theLength = 0 # Always the length of _treeOrder self._theIndex = 0 # The current iterator index self._treeChanged = False # True if tree structure has changed self._handleSeed = None # Used for generating handles for testing @@ -68,7 +67,6 @@ class NWTree(): self._treeRoots = [] self._trashRoot = None self._archRoot = None - self._theLength = 0 self._theIndex = 0 self._treeChanged = False return @@ -81,8 +79,8 @@ class NWTree(): def append(self, tHandle, pHandle, nwItem): """Add a new item to the end of the tree. """ - tHandle = checkString(tHandle, None, True) - pHandle = checkString(pHandle, None, True) + tHandle = checkHandle(tHandle, None, True) + pHandle = checkHandle(pHandle, None, True) if tHandle is None: tHandle = self._makeHandle() @@ -108,16 +106,16 @@ class NWTree(): else: logger.error("Only one trash folder allowed") - self._theLength = len(self._treeOrder) self._setTreeChanged(True) return def packXML(self, xParent): - """Pack the content of the tree into an XML object. + """Pack the content of the tree into the provided XML object. In + the order defined by the _treeOrder list. """ xContent = etree.SubElement(xParent, "content", attrib={ - "count": str(self._theLength)} + "count": str(len(self._treeOrder))} ) for tHandle in self._treeOrder: tItem = self.__getitem__(tHandle) @@ -309,7 +307,6 @@ class NWTree(): # Save the temp list self._treeOrder = tmpOrder - self._theLength = len(self._treeOrder) self._setTreeChanged(True) logger.verbose("Project tree order updated") @@ -330,7 +327,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 @@ -370,12 +367,12 @@ class NWTree(): def __len__(self): """Return the length counter. Does not check that it is correct! """ - return self._theLength + return len(self._treeOrder) def __bool__(self): """Returns True if the tree has any entries. """ - return self._theLength > 0 + return len(self._treeOrder) > 0 ## # Item Access Methods @@ -391,19 +388,21 @@ class NWTree(): return None def __delitem__(self, tHandle): - """This only removes the item from the order list, but not from - the project tree. + """Remove an item from the internal lists and dictionaries. """ - if tHandle not in self._treeOrder: - logger.warning( - "Could not remove item %s from project tree as it does not exist" % tHandle - ) + if tHandle in self._treeOrder and tHandle in self._projTree: + self._treeOrder.remove(tHandle) + del self._projTree[tHandle] + else: + logger.warning("Failed to delete item %s: item not found" % tHandle) return False - self._treeOrder.remove(tHandle) - self._theLength = len(self._treeOrder) if tHandle in self._treeRoots: self._treeRoots.remove(tHandle) + if tHandle == self._trashRoot: + self._trashRoot = None + if tHandle == self._archRoot: + self._archRoot = None self._setTreeChanged(True) @@ -427,7 +426,7 @@ class NWTree(): def __next__(self): """Returns the item from the next entry in the _treeOrder list. """ - if self._theIndex < self._theLength: + if self._theIndex < len(self._treeOrder): theItem = self.__getitem__(self._treeOrder[self._theIndex]) self._theIndex += 1 return theItem diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 28d23102..0ca21507 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -378,11 +378,11 @@ class GuiProjectTree(QTreeWidget): return True def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False): - """Delete items from the tree. Note that this does not delete - the item from the item tree in the project object. However, - since this is only meta data, there isn't really a need to do - that to save memory. Items not in the tree are not saved to the - project file, so a loaded project will be clean anyway. + """Delete an item from the project tree. As a first step, files are + moved to the Trash folder. Permanent deletion is a second step. This + second step also deletes the item from the project object as well as + delete the files on disk. Folders are deleted if they're empty only, + and the deletion is always permanent. """ if tHandle is None: tHandle = self.getSelectedHandle() diff --git a/tests/test_common.py b/tests/test_common.py index 94e384db..5a73ad45 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -5,7 +5,7 @@ import pytest from nw.common import ( checkString, checkBool, checkInt, colRange, formatInt, transferCase, - fuzzyTime + fuzzyTime, checkHandle ) from nwtools import cmpList @@ -42,6 +42,15 @@ def testCheckBool(): assert checkBool(1.0, None, False) is None assert checkBool(2.0, None, False) is None +@pytest.mark.core +def testCheckHandle(): + assert checkHandle("None", 1, True) is None + assert checkHandle("None", 1, False) == 1 + assert checkHandle(None, 1, True) is None + assert checkHandle(None, 1, False) == 1 + assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf" + assert checkHandle("h7666c91c7ccf", None, False) is None + @pytest.mark.core def testColRange(): assert colRange([0, 0], [0, 0], 0) is None From 904bdd9001e4cdd0eedd3b1a22943a72961627cb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 17 Sep 2020 14:05:51 +0200 Subject: [PATCH 2/7] Minor code improvements and consistent naming of XML functions --- nw/core/project.py | 17 +++++++++-------- nw/core/spellcheck.py | 7 ++----- nw/core/status.py | 4 ++-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index dd3f8c05..d38d111b 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -481,7 +481,7 @@ class NWProject(): # Changes: # 1.0 : Original file format. - # 1.1 : Changes the way documents are structure in the project + # 1.1 : Changes the way documents are structured in the project # folder from data_X, where X is the first hex value of # the handle, to a single content folder. # 1.2 : Changes the way autoReplace entries are stored. The 1.1 @@ -518,15 +518,16 @@ class NWProject(): msgRes = msgBox.question(self.theParent, "Version Conflict", ( "This project was saved by a newer version of novelWriter, version %s. " "This is version %s. If you continue to open the project, some attributes " - "and settings may not be preserved. Continue opening the project?" + "and settings may not be preserved, but the overall project should be fine. " + "Continue opening the project?" ) % ( appVersion, nw.__version__ )) if msgRes != QMessageBox.Yes: return False - # Start Parsing XML - # ================= + # Start Parsing the XML + # ===================== for xChild in xRoot: if xChild.tag == "project": @@ -578,9 +579,9 @@ class NWProject(): elif xItem.tag == "notesWordCount": self.notesWCount = checkInt(xItem.text, 0, False) elif xItem.tag == "status": - self.statusItems.unpackEntries(xItem) + self.statusItems.unpackXML(xItem) elif xItem.tag == "importance": - self.importItems.unpackEntries(xItem) + self.importItems.unpackXML(xItem) elif xItem.tag == "autoReplace": for xEntry in xItem: if xEntry.tag == "entry": @@ -684,9 +685,9 @@ class NWProject(): self._packProjectValue(xTitleFmt, aKey, aValue) xStatus = etree.SubElement(xSettings, "status") - self.statusItems.packEntries(xStatus) + self.statusItems.packXML(xStatus) xStatus = etree.SubElement(xSettings, "importance") - self.importItems.packEntries(xStatus) + self.importItems.packXML(xStatus) # Save Tree Content logger.debug("Writing project content") diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index e904eae5..a3a51db6 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -89,13 +89,10 @@ class NWSpellCheck(): @staticmethod def expandLanguage(spTag): - """Translate a language tag to something more suer friendly. + """Translate a language tag to something more user friendly. """ spBits = spTag.split("_") - if spBits[0] in isoLanguage.ISO_639_1: - spLang = isoLanguage.ISO_639_1[spBits[0]] - else: - spLang = spBits[0] + spLang = isoLanguage.ISO_639_1.get(spBits[0], spBits[0]) if len(spBits) > 1: spLang += " (%s)" % spBits[1] return spLang diff --git a/nw/core/status.py b/nw/core/status.py index eac1a52a..81496743 100644 --- a/nw/core/status.py +++ b/nw/core/status.py @@ -115,7 +115,7 @@ class NWStatus(): self.theCounts[theIndex] += 1 return - def packEntries(self, xParent): + def packXML(self, xParent): """Pack the status entries into an XML object for saving to the main project file. """ @@ -128,7 +128,7 @@ class NWStatus(): xSub.text = self.theLabels[n] return True - def unpackEntries(self, xParent): + def unpackXML(self, xParent): """Unpack an XML tree and set the class values. """ theLabels = [] From d7feece88709d727e9f935dd697bc784f15dc7af Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 18 Sep 2020 15:20:27 +0200 Subject: [PATCH 3/7] Improvements to commenst and logtext --- nw/core/project.py | 35 ++++++++++++++++++----------------- nw/core/tree.py | 8 ++++---- nw/gui/docviewer.py | 11 +++++++---- nw/gui/projtree.py | 4 ++-- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/nw/core/project.py b/nw/core/project.py index d38d111b..5c187d7c 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1441,23 +1441,24 @@ class NWProject(): def _deprecatedFiles(self): """Delete files that are no longer used by novelWriter. """ - rmList = [] - rmList.append(path.join(self.projCache, "nwProject.nwx.0")) - rmList.append(path.join(self.projCache, "nwProject.nwx.1")) - rmList.append(path.join(self.projCache, "nwProject.nwx.2")) - rmList.append(path.join(self.projCache, "nwProject.nwx.3")) - rmList.append(path.join(self.projCache, "nwProject.nwx.4")) - rmList.append(path.join(self.projCache, "nwProject.nwx.5")) - rmList.append(path.join(self.projCache, "nwProject.nwx.6")) - rmList.append(path.join(self.projCache, "nwProject.nwx.7")) - rmList.append(path.join(self.projCache, "nwProject.nwx.8")) - rmList.append(path.join(self.projCache, "nwProject.nwx.9")) - rmList.append(path.join(self.projMeta, "mainOptions.json")) - rmList.append(path.join(self.projMeta, "exportOptions.json")) - rmList.append(path.join(self.projMeta, "outlineOptions.json")) - rmList.append(path.join(self.projMeta, "timelineOptions.json")) - rmList.append(path.join(self.projMeta, "docMergeOptions.json")) - rmList.append(path.join(self.projMeta, "sessionLogOptions.json")) + rmList = [ + path.join(self.projCache, "nwProject.nwx.0"), + path.join(self.projCache, "nwProject.nwx.1"), + path.join(self.projCache, "nwProject.nwx.2"), + path.join(self.projCache, "nwProject.nwx.3"), + path.join(self.projCache, "nwProject.nwx.4"), + path.join(self.projCache, "nwProject.nwx.5"), + path.join(self.projCache, "nwProject.nwx.6"), + path.join(self.projCache, "nwProject.nwx.7"), + path.join(self.projCache, "nwProject.nwx.8"), + path.join(self.projCache, "nwProject.nwx.9"), + path.join(self.projMeta, "mainOptions.json"), + path.join(self.projMeta, "exportOptions.json"), + path.join(self.projMeta, "outlineOptions.json"), + path.join(self.projMeta, "timelineOptions.json"), + path.join(self.projMeta, "docMergeOptions.json"), + path.join(self.projMeta, "sessionLogOptions.json"), + ] for rmFile in rmList: if path.isfile(rmFile): diff --git a/nw/core/tree.py b/nw/core/tree.py index e9e39820..820d12d8 100644 --- a/nw/core/tree.py +++ b/nw/core/tree.py @@ -84,7 +84,7 @@ class NWTree(): if tHandle is None: tHandle = self._makeHandle() - logger.verbose("Adding entry %s with parent %s" % (str(tHandle), str(pHandle))) + logger.verbose("Adding item %s with parent %s" % (str(tHandle), str(pHandle))) nwItem.setHandle(tHandle) nwItem.setParent(pHandle) @@ -93,15 +93,15 @@ class NWTree(): self._treeOrder.append(tHandle) if nwItem.itemType == nwItemType.ROOT: - logger.verbose("Entry %s is a root item" % str(tHandle)) + logger.verbose("Item %s is a root item" % str(tHandle)) self._treeRoots.append(tHandle) if nwItem.itemClass == nwItemClass.ARCHIVE: - logger.verbose("Entry %s is the archive folder" % str(tHandle)) + logger.verbose("Item %s is the archive folder" % str(tHandle)) self._archRoot = tHandle if nwItem.itemType == nwItemType.TRASH: if self._trashRoot is None: - logger.verbose("Entry %s is the trash folder" % str(tHandle)) + logger.verbose("Item %s is the trash folder" % str(tHandle)) self._trashRoot = tHandle else: logger.error("Only one trash folder allowed") diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index c54411b4..09f83719 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -639,7 +639,8 @@ class GuiDocViewHistory(): return def _truncateHistory(self, atPos): - """Truncate the navigation history to the given position. + """Truncate the navigation history to the given position. Also + enforces a maximum length of the navigation history to 20. """ nSkip = 1 if atPos > 19 else 0 @@ -652,13 +653,15 @@ class GuiDocViewHistory(): return def _dumpHistory(self): - """Debug function to dump history. Since it is a for loop, it is - skipped entirely if log level isn't VERBOSE. + """Debug function to dump history to the logger. Since it is a + for loop, it is skipped entirely if log level isn't VERBOSE. """ if logger.getEffectiveLevel() < logging.DEBUG: for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)): logger.verbose( - "History: %s %2d %13s %5d" % (">" if i == self._currPos else " ", i, h, p) + "History %02d: %s %13s [x:%d]" % ( + i + 1, ">" if i == self._currPos else " ", h, p + ) ) return diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 0ca21507..0c909a91 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -584,7 +584,7 @@ class GuiProjectTree(QTreeWidget): always make sure items with a parent have had their parent item sent first. """ - logger.debug("Building project tree ...") + logger.debug("Building the project tree ...") self.clear() iCount = 0 @@ -592,7 +592,7 @@ class GuiProjectTree(QTreeWidget): iCount += 1 self._addTreeItem(nwItem) - logger.debug("%d items added to project tree" % iCount) + logger.debug("%d items added to the project tree" % iCount) return True def getSelectedHandle(self): From 86bb1fc9a42bd0ab7f007c85baf461edb24c3b3b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 18 Sep 2020 17:10:33 +0200 Subject: [PATCH 4/7] Made some the properties of the NWDoc class internal --- nw/core/__init__.py | 2 -- nw/core/document.py | 74 ++++++++++++++++++++++++------------------- nw/gui/doceditor.py | 32 ++++++++++--------- tests/test_project.py | 2 +- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/nw/core/__init__.py b/nw/core/__init__.py index 91616302..ede01b73 100644 --- a/nw/core/__init__.py +++ b/nw/core/__init__.py @@ -4,7 +4,6 @@ from nw.core.document import NWDoc from nw.core.index import NWIndex from nw.core.project import NWProject from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple -from nw.core.tokenizer import Tokenizer from nw.core.tohtml import ToHtml from nw.core.tools import countWords, numberToRoman, numberToWord @@ -15,7 +14,6 @@ __all__ = [ "NWSpellCheck", "NWSpellEnchant", "NWSpellSimple", - "Tokenizer", "ToHtml", "countWords", "numberToRoman", diff --git a/nw/core/document.py b/nw/core/document.py index 6c859168..45df4cc1 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -26,11 +26,9 @@ """ import logging -import nw from os import path, rename, unlink -from nw.core.item import NWItem from nw.constants import nwAlert from nw.common import isHandle from nw.constants import nwItemLayout, nwItemClass, nwConst @@ -41,14 +39,14 @@ class NWDoc(): def __init__(self, theProject, theParent): - self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent - self.theItem = None - self.docHandle = None - self.fileLoc = None - self.docMeta = "" + # Internal Variables + self._theItem = None # The currently open item + self._docHandle = None # The handle of the currently open item + self._fileLoc = None # The file location of the currently open item + self._docMeta = "" # The meta string of the currently open item # Internal Mapping self.makeAlert = self.theParent.makeAlert @@ -62,10 +60,10 @@ class NWDoc(): def clearDocument(self): """Clear the document contents. """ - self.theItem = None - self.docHandle = None - self.fileLoc = None - self.docMeta = "" + self._theItem = None + self._docHandle = None + self._fileLoc = None + self._docMeta = "" return def openDocument(self, tHandle, showStatus=True, isOrphan=False): @@ -78,31 +76,31 @@ class NWDoc(): # Always clear first, since the object will often be reused. self.clearDocument() - self.docHandle = tHandle + self._docHandle = tHandle if not isOrphan: - self.theItem = self.theProject.projTree[tHandle] + self._theItem = self.theProject.projTree[tHandle] else: - self.theItem = None + self._theItem = None - if self.theItem is None and not isOrphan: + if self._theItem is None and not isOrphan: self.clearDocument() return None - docFile = self.docHandle+".nwd" + docFile = self._docHandle+".nwd" logger.debug("Opening document %s" % docFile) docPath = path.join(self.theProject.projContent, docFile) - self.fileLoc = docPath + self._fileLoc = docPath theText = "" - self.docMeta = "" + self._docMeta = "" if path.isfile(docPath): try: with open(docPath, mode="r", encoding="utf8") as inFile: fstLine = inFile.readline() if fstLine.startswith("%%~ "): # This is the meta line - self.docMeta = fstLine[4:].strip() + self._docMeta = fstLine[4:].strip() else: theText = fstLine theText += inFile.read() @@ -120,10 +118,10 @@ class NWDoc(): logger.debug("The requested document does not exist.") return "" - logger.verbose("DocMeta: '%s'" % self.docMeta) + logger.verbose("DocMeta: '%s'" % self._docMeta) if showStatus and not isOrphan: - self.theParent.statusBar.setStatus("Opened Document: %s" % self.theItem.itemName) + self.theParent.statusBar.setStatus("Opened Document: %s" % self._theItem.itemName) return theText @@ -131,29 +129,29 @@ class NWDoc(): """Save the document via temp file in case of save failure, and in any case keep a backup of the file. """ - if self.docHandle is None: + if self._docHandle is None: return False self.theProject.ensureFolderStructure() - docFile = self.docHandle+".nwd" + docFile = self._docHandle+".nwd" logger.debug("Saving document %s" % docFile) docPath = path.join(self.theProject.projContent, docFile) docTemp = path.join(self.theProject.projContent, docFile+"~") - if isinstance(self.theItem, NWItem): - itemPath = self.theProject.projTree.getItemPath(self.docHandle) + if self._theItem is None: + docMeta = "" + else: + itemPath = self.theProject.projTree.getItemPath(self._docHandle) docMeta = ( "%%~ {handlepath:s}:{itemclass:s}:{itemlayout:s}:{itemname:s}\n" ).format( handlepath = ":".join(itemPath), - itemclass = self.theItem.itemClass.name, - itemlayout = self.theItem.itemLayout.name, - itemname = self.theItem.itemName, + itemclass = self._theItem.itemClass.name, + itemlayout = self._theItem.itemLayout.name, + itemname = self._theItem.itemName, ) - else: - docMeta = "" try: with open(docTemp, mode="w", encoding="utf8") as outFile: @@ -169,7 +167,7 @@ class NWDoc(): unlink(docPath) rename(docTemp, docPath) - self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) + self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName) return True @@ -201,15 +199,25 @@ class NWDoc(): # Getters ## + def getFileLocation(self): + """Return the file location of the current file. + """ + return self._fileLoc + + def getCurrentItem(self): + """Return a pointer to the currently open item. + """ + return self._theItem + def getMeta(self): """Parses the document meta tag and returns the path and name as a list and a string. """ - if len(self.docMeta) < 14: + if len(self._docMeta) < 14: # Not enough information return "", [], None, None - theMeta = self.docMeta + theMeta = self._docMeta # Scan for handles thePath = [] diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 0da72625..a8e9de97 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -270,10 +270,12 @@ class GuiDocEditor(QTextEdit): afTime = time() logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) - if tLine is None: - self.setCursorPosition(self.nwDocument.theItem.cursorPos) + theItem = self.nwDocument.getCurrentItem() + if tLine is None and theItem is not None: + self.setCursorPosition(theItem.cursorPos) else: self.setCursorLine(tLine) + self.lastEdit = time() self._runCounter() self.wcTimer.start() @@ -308,21 +310,20 @@ class GuiDocEditor(QTextEdit): """Save the text currently in the editor to the NWDoc object, and update the NWItem meta data. """ - if self.nwDocument.theItem is None: + theItem = self.nwDocument.getCurrentItem() + if theItem is None: return False docText = self.getText() cursPos = self.getCursorPosition() - self.nwDocument.theItem.setCharCount(self.charCount) - self.nwDocument.theItem.setWordCount(self.wordCount) - self.nwDocument.theItem.setParaCount(self.paraCount) - self.nwDocument.theItem.setCursorPos(cursPos) + theItem.setCharCount(self.charCount) + theItem.setWordCount(self.wordCount) + theItem.setParaCount(self.paraCount) + theItem.setCursorPos(cursPos) self.nwDocument.saveDocument(docText) self.setDocumentChanged(False) - self.theParent.theIndex.scanText( - self.nwDocument.theItem.itemHandle, docText - ) + self.theParent.theIndex.scanText(theItem.itemHandle, docText) return True @@ -598,7 +599,7 @@ class GuiDocEditor(QTextEdit): "Location: {fileLoc:s}" ).format( handle = self.theHandle, - fileLoc = str(self.nwDocument.fileLoc) + fileLoc = str(self.nwDocument.getFileLocation()) )) return @@ -873,7 +874,8 @@ class GuiDocEditor(QTextEdit): def _updateCounts(self): """Slot for the word counter's finished signal """ - if self.theHandle is None or self.nwDocument.theItem is None: + theItem = self.nwDocument.getCurrentItem() + if self.theHandle is None or theItem is None: return logger.verbose("Updating word count") @@ -881,9 +883,9 @@ class GuiDocEditor(QTextEdit): self.charCount = self.wCounter.charCount self.wordCount = self.wCounter.wordCount self.paraCount = self.wCounter.paraCount - self.nwDocument.theItem.setCharCount(self.charCount) - self.nwDocument.theItem.setWordCount(self.wordCount) - self.nwDocument.theItem.setParaCount(self.paraCount) + theItem.setCharCount(self.charCount) + theItem.setWordCount(self.wordCount) + theItem.setParaCount(self.paraCount) self.theParent.treeView.propagateCount(self.theHandle, self.wordCount) self.theParent.treeView.projectWordCount() diff --git a/tests/test_project.py b/tests/test_project.py index db24da92..784e9f8d 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -316,7 +316,7 @@ def testDocMeta(nwDummy, nwLipsum): assert theClass == nwItemClass.NOVEL assert theLayout == nwItemLayout.SCENE - aDoc.docMeta = "too_short" + aDoc._docMeta = "too_short" theMeta, thePath, theClass, theLayout = aDoc.getMeta() assert theMeta == "" assert thePath == [] From bb59891310e8463a791f2f061f409cfe4e1379cc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 18 Sep 2020 17:57:31 +0200 Subject: [PATCH 5/7] Cleanup in index and options and improved options test coverage --- nw/core/index.py | 3 ++- nw/core/options.py | 3 --- tests/test_project.py | 61 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index d50533a1..29126bed 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -27,6 +27,7 @@ import logging import json +import nw from os import path from time import time @@ -66,9 +67,9 @@ class NWIndex(): def __init__(self, theProject, theParent): # Internal + self.mainConf = nw.CONFIG self.theProject = theProject self.theParent = theParent - self.mainConf = self.theParent.mainConf self.indexBroken = False # Indices diff --git a/nw/core/options.py b/nw/core/options.py index 9a58bf7c..314a75c6 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -28,7 +28,6 @@ import logging import json -import nw from os import path @@ -40,9 +39,7 @@ class OptionState(): def __init__(self, theProject): - self.mainConf = nw.CONFIG self.theProject = theProject - self.theState = {} self.validMap = { "GuiWritingStats": { diff --git a/tests/test_project.py b/tests/test_project.py index 784e9f8d..54d44d6a 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -12,7 +12,7 @@ from nw.core.project import NWProject from nw.core.document import NWDoc from nw.core.index import NWIndex from nw.core.spellcheck import NWSpellEnchant, NWSpellSimple -from nw.constants import nwItemClass, nwItemLayout +from nw.constants import nwItemClass, nwItemLayout, nwFiles @pytest.mark.project def testProjectNewOpenSave(nwFuncTemp, nwTempProj, nwRef, nwTemp, nwDummy): @@ -372,3 +372,62 @@ def testSpellSimple(nwTemp, nwConf): dList = spChk.listDictionaries() assert len(dList) > 0 + +@pytest.mark.project +def testProjectOptions(nwDummy, nwLipsum): + theProject = NWProject(nwDummy) + assert theProject.projMeta is None + + theOpts = theProject.optState + assert not theOpts.loadSettings() + assert not theOpts.saveSettings() + + # No Settings + assert theProject.openProject(nwLipsum) + assert theOpts.loadSettings() + assert theOpts.saveSettings() + assert str(theOpts.theState) == r"{}" + + # Read Invalid Settings and Filter + stateFile = path.join(theProject.projMeta, nwFiles.OPTS_FILE) + with open(stateFile, mode="w", encoding="utf8") as outFile: + outFile.write( + r'{"GuiProjectSettings": {"winWidth": 100, "winHeight": 50}, "NoGroup": {"NoName": 0}}' + ) + assert theOpts.loadSettings() + assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 100, 'winHeight': 50}}" + + # Set New Settings + assert not theOpts.setValue("NoGroup", "NoName", None) + assert not theOpts.setValue("GuiProjectSettings", "NoName", None) + assert theOpts.setValue("GuiProjectSettings", "winWidth", 200) + assert theOpts.setValue("GuiProjectSettings", "winHeight", 80) + assert str(theOpts.theState) == r"{'GuiProjectSettings': {'winWidth': 200, 'winHeight': 80}}" + + # Check Read/Write Types + + ## String + assert theOpts.setValue("GuiWritingStats", "winWidth", "123") + assert isinstance(theOpts.getString("GuiWritingStats", "winWidth", "456"), str) + assert theOpts.getString("GuiWritingStats", "NoName", "456") == "456" + + ## Int + assert theOpts.setValue("GuiWritingStats", "winWidth", "123") + assert isinstance(theOpts.getInt("GuiWritingStats", "winWidth", 456), int) + assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456 + assert theOpts.setValue("GuiWritingStats", "winWidth", "True") + assert theOpts.getInt("GuiWritingStats", "NoName", 456) == 456 + + ## Float + assert theOpts.setValue("GuiWritingStats", "winWidth", "123") + assert isinstance(theOpts.getFloat("GuiWritingStats", "winWidth", 456.0), float) + assert theOpts.getFloat("GuiWritingStats", "NoName", 456.0) == 456.0 + assert theOpts.setValue("GuiWritingStats", "winWidth", "True") + assert theOpts.getFloat("GuiWritingStats", "winWidth", 456.0) == 456.0 + + ## Bool + assert theOpts.setValue("GuiWritingStats", "winWidth", True) + assert isinstance(theOpts.getBool("GuiWritingStats", "winWidth", False), bool) + assert theOpts.getFloat("GuiWritingStats", "NoName", False) is False + assert theOpts.setValue("GuiWritingStats", "winWidth", "True") + assert theOpts.getFloat("GuiWritingStats", "winWidth", False) is False From 4e03fdca3148bcfc6f355fb7498f79599d607dd4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 18 Sep 2020 18:30:52 +0200 Subject: [PATCH 6/7] Added tests for core tools functions --- tests/test_tools.py | 84 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_tools.py diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 00000000..bd79fb69 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""novelWriter Tools Tester +""" + +import pytest + +from nw.core.tools import countWords, numberToRoman, numberToWord + +@pytest.mark.core +def testCountWords(): + testText = ( + "# Heading One\n" + "## Heading Two\n" + "### Heading Three\n" + "#### Heading Four\n" + "\n" + "@tag: value\n" + "\n" + "% A comment that should n ot be counted.\n" + "\n" + "The first paragraph.\n" + "\n" + "The second paragraph.\n" + "\n" + "\n" + "The third paragraph.\n" + ) + cC, wC, pC = countWords(testText) + + assert cC == 108 + assert wC == 17 + assert pC == 3 + +@pytest.mark.core +def testNumberWords(): + assert numberToWord(0, "en") == "Zero" + assert numberToWord(1, "en") == "One" + assert numberToWord(2, "en") == "Two" + assert numberToWord(3, "en") == "Three" + assert numberToWord(4, "en") == "Four" + assert numberToWord(5, "en") == "Five" + assert numberToWord(6, "en") == "Six" + assert numberToWord(7, "en") == "Seven" + assert numberToWord(8, "en") == "Eight" + assert numberToWord(9, "en") == "Nine" + assert numberToWord(10, "en") == "Ten" + assert numberToWord(11, "en") == "Eleven" + assert numberToWord(12, "en") == "Twelve" + assert numberToWord(13, "en") == "Thirteen" + assert numberToWord(14, "en") == "Fourteen" + assert numberToWord(15, "en") == "Fifteen" + assert numberToWord(16, "en") == "Sixteen" + assert numberToWord(17, "en") == "Seventeen" + assert numberToWord(18, "en") == "Eighteen" + assert numberToWord(19, "en") == "Nineteen" + assert numberToWord(20, "en") == "Twenty" + assert numberToWord(21, "en") == "Twenty-One" + assert numberToWord(29, "en") == "Twenty-Nine" + assert numberToWord(42, "en") == "Forty-Two" + assert numberToWord(142, "en") == "One Hundred Forty-Two" + assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine" + +@pytest.mark.core +def testRomanNumbers(): + assert numberToRoman(None, False) == "NAN" + assert numberToRoman(0, False) == "OOR" + assert numberToRoman(1, False) == "I" + assert numberToRoman(2, False) == "II" + assert numberToRoman(3, False) == "III" + assert numberToRoman(4, False) == "IV" + assert numberToRoman(5, False) == "V" + assert numberToRoman(6, False) == "VI" + assert numberToRoman(7, False) == "VII" + assert numberToRoman(8, False) == "VIII" + assert numberToRoman(9, False) == "IX" + assert numberToRoman(10, False) == "X" + assert numberToRoman(14, False) == "XIV" + assert numberToRoman(42, False) == "XLII" + assert numberToRoman(99, False) == "XCIX" + assert numberToRoman(142, False) == "CXLII" + assert numberToRoman(542, False) == "DXLII" + assert numberToRoman(999, False) == "CMXCIX" + assert numberToRoman(2010, False) == "MMX" + assert numberToRoman(999, True) == "cmxcix" From de72d93b9c307884324f0e45bb3f003c068429d2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 18 Sep 2020 18:50:13 +0200 Subject: [PATCH 7/7] Minor cleanup, and made status class properties private --- nw/core/status.py | 80 ++++++++++++++++++++++---------------------- nw/core/tohtml.py | 3 +- nw/core/tokenizer.py | 6 ++-- 3 files changed, 43 insertions(+), 46 deletions(-) diff --git a/nw/core/status.py b/nw/core/status.py index 81496743..c495e93c 100644 --- a/nw/core/status.py +++ b/nw/core/status.py @@ -36,12 +36,12 @@ logger = logging.getLogger(__name__) class NWStatus(): def __init__(self): - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 + self._theLabels = [] + self._theColours = [] + self._theCounts = [] + self._theMap = {} + self._theLength = 0 + self._theIndex = 0 return def addEntry(self, theLabel, theColours): @@ -50,11 +50,11 @@ class NWStatus(): """ theLabel = theLabel.strip() if self.lookupEntry(theLabel) is None: - self.theLabels.append(theLabel) - self.theColours.append(theColours) - self.theCounts.append(0) - self.theMap[theLabel] = self.theLength - self.theLength += 1 + self._theLabels.append(theLabel) + self._theColours.append(theColours) + self._theCounts.append(0) + self._theMap[theLabel] = self._theLength + self._theLength += 1 return True def lookupEntry(self, theLabel): @@ -64,8 +64,8 @@ class NWStatus(): if theLabel is None: return None theLabel = theLabel.strip() - if theLabel in self.theMap.keys(): - return self.theMap[theLabel] + if theLabel in self._theMap.keys(): + return self._theMap[theLabel] return None def checkEntry(self, theStatus): @@ -77,8 +77,8 @@ class NWStatus(): if self.lookupEntry(theStatus) is not None: return theStatus theStatus = checkInt(theStatus, 0, False) - if theStatus >= 0 and theStatus < self.theLength: - return self.theLabels[theStatus] + if theStatus >= 0 and theStatus < self._theLength: + return self._theLabels[theStatus] def setNewEntries(self, newList): """Update the list of entries after they have been modified by @@ -87,12 +87,12 @@ class NWStatus(): replaceMap = {} if newList is not None: - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 + self._theLabels = [] + self._theColours = [] + self._theCounts = [] + self._theMap = {} + self._theLength = 0 + self._theIndex = 0 for nName, nR, nG, nB, oName in newList: self.addEntry(nName, (nR, nG, nB)) @@ -104,7 +104,7 @@ class NWStatus(): def resetCounts(self): """Clear the counts of references to the status entries. """ - self.theCounts = [0]*self.theLength + self._theCounts = [0]*self._theLength return def countEntry(self, theLabel): @@ -112,20 +112,20 @@ class NWStatus(): """ theIndex = self.lookupEntry(theLabel) if theIndex is not None: - self.theCounts[theIndex] += 1 + self._theCounts[theIndex] += 1 return def packXML(self, xParent): """Pack the status entries into an XML object for saving to the main project file. """ - for n in range(self.theLength): + for n in range(self._theLength): xSub = etree.SubElement(xParent, "entry", attrib={ - "blue" : str(self.theColours[n][2]), - "green" : str(self.theColours[n][1]), - "red" : str(self.theColours[n][0]), + "blue" : str(self._theColours[n][2]), + "green" : str(self._theColours[n][1]), + "red" : str(self._theColours[n][0]), }) - xSub.text = self.theLabels[n] + xSub.text = self._theLabels[n] return True def unpackXML(self, xParent): @@ -151,12 +151,12 @@ class NWStatus(): theColours.append((cR, cG, cB)) if len(theLabels) > 0: - self.theLabels = [] - self.theColours = [] - self.theCounts = [] - self.theMap = {} - self.theLength = 0 - self.theIndex = 0 + self._theLabels = [] + self._theColours = [] + self._theCounts = [] + self._theMap = {} + self._theLength = 0 + self._theIndex = 0 for n in range(len(theLabels)): self.addEntry(theLabels[n], theColours[n]) @@ -170,22 +170,22 @@ class NWStatus(): def __getitem__(self, n): """Return an entry by its index. """ - if n >= 0 and n < self.theLength: - return self.theLabels[n], self.theColours[n], self.theCounts[n] + if n >= 0 and n < self._theLength: + return self._theLabels[n], self._theColours[n], self._theCounts[n] return None, None, None def __iter__(self): """Initialise the iterator. """ - self.theIndex = 0 + self._theIndex = 0 return self def __next__(self): """Return the next entry for the iterator. """ - if self.theIndex < self.theLength: - theLabel, theColour, theCount = self.__getitem__(self.theIndex) - self.theIndex += 1 + if self._theIndex < self._theLength: + theLabel, theColour, theCount = self.__getitem__(self._theIndex) + self._theIndex += 1 return theLabel, theColour, theCount else: raise StopIteration diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 720ac9cd..7190bb9e 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -309,8 +309,7 @@ class ToHtml(Tokenizer): def _formatKeywords(self, tText): """Apply HTML formatting to keywords. """ - tText = "@"+tText - isValid, theBits, thePos = self.theParent.theIndex.scanThis(tText) + isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText) if not isValid or not theBits: return "" diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 5db2d19d..f1c6a988 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -27,7 +27,6 @@ import logging import re -import nw from operator import itemgetter from PyQt5.QtCore import QRegularExpression @@ -74,9 +73,8 @@ class Tokenizer(): def __init__(self, theProject, theParent): - self.mainConf = nw.CONFIG - self.theProject = theProject - self.theParent = theParent + self.theProject = theProject + self.theParent = theParent # Data Variables self.theText = None # The raw text to be tokenized