Merge branch 'main' into release_2.0rc2

This commit is contained in:
Veronica Berglyd Olsen
2022-11-12 18:30:00 +01:00
committed by GitHub
7 changed files with 116 additions and 49 deletions
+32 -20
View File
@@ -58,23 +58,23 @@ class NWIndex:
The index data is cached in a JSON file between writing sessions. The index data is cached in a JSON file between writing sessions.
""" """
def __init__(self, theProject): def __init__(self, project):
self.theProject = theProject self._project = project
# Storage and State # Storage and State
self._tagsIndex = TagsIndex() self._tagsIndex = TagsIndex()
self._itemIndex = ItemIndex(theProject) self._itemIndex = ItemIndex(project)
self._indexBroken = False self._indexBroken = False
# TimeStamps # TimeStamps
self._indexChange = 0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
return return
def __repr__(self): def __repr__(self):
return f"<NWIndex project='{self.theProject.data.name}'>" return f"<NWIndex project='{self._project.data.name}'>"
## ##
# Properties # Properties
@@ -93,10 +93,22 @@ class NWIndex:
""" """
self._tagsIndex.clear() self._tagsIndex.clear()
self._itemIndex.clear() self._itemIndex.clear()
self._indexChange = 0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
return return
def rebuildIndex(self):
"""Rebuild the entire index from scratch.
"""
self.clearIndex()
for nwItem in self._project.tree:
if nwItem is not None and nwItem.isFileType():
tHandle = nwItem.itemHandle
theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "")
self._indexBroken = False
return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle. """Delete all entries of a given document handle.
""" """
@@ -113,11 +125,11 @@ class NWIndex:
moved from the archive or trash folders back into the active moved from the archive or trash folders back into the active
project. project.
""" """
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): if not self._project.tree.checkType(tHandle, nwItemType.FILE):
return False return False
logger.debug("Re-indexing item '%s'", tHandle) logger.debug("Re-indexing item '%s'", tHandle)
theDoc = self.theProject.storage.getDocument(tHandle) theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "") self.scanText(tHandle, theDoc.readDocument() or "")
return True return True
@@ -125,13 +137,13 @@ class NWIndex:
def indexChangedSince(self, checkTime): def indexChangedSince(self, checkTime):
"""Check if the index has changed since a given time. """Check if the index has changed since a given time.
""" """
return self._indexChange > checkTime return self._indexChange > float(checkTime)
def rootChangedSince(self, rootHandle, checkTime): def rootChangedSince(self, rootHandle, checkTime):
"""Check if the index has changed since a given time for a """Check if the index has changed since a given time for a
given root item. given root item.
""" """
return self._rootChange.get(rootHandle, self._indexChange) > checkTime return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
## ##
# Load and Save Index to/from File # Load and Save Index to/from File
@@ -140,7 +152,7 @@ class NWIndex:
def loadIndex(self): def loadIndex(self):
"""Load index from last session from the project meta folder. """Load index from last session from the project meta folder.
""" """
indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE) indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path): if not isinstance(indexFile, Path):
return False return False
@@ -171,12 +183,12 @@ class NWIndex:
logger.debug("Checking index") logger.debug("Checking index")
# Check that all files are indexed # Check that all files are indexed
for fHandle in self.theProject.projFiles: for fHandle in self._project.projFiles:
if fHandle not in self._itemIndex: if fHandle not in self._itemIndex:
logger.warning("Item '%s' is not in the index", fHandle) logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle) self.reIndexHandle(fHandle)
self._indexChange = round(time()) self._indexChange = time()
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000) logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
@@ -186,7 +198,7 @@ class NWIndex:
"""Save the current index as a json file in the project meta """Save the current index as a json file in the project meta
data folder. data folder.
""" """
indexFile = self.theProject.storage.getMetaFile(nwFiles.INDEX_FILE) indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path): if not isinstance(indexFile, Path):
return False return False
@@ -222,7 +234,7 @@ class NWIndex:
files before we save them, in which case we already have the files before we save them, in which case we already have the
text. text.
""" """
theItem = self.theProject.tree[tHandle] theItem = self._project.tree[tHandle]
if theItem is None: if theItem is None:
logger.info("Not indexing unknown item '%s'", tHandle) logger.info("Not indexing unknown item '%s'", tHandle)
return False return False
@@ -256,7 +268,7 @@ class NWIndex:
self._scanActive(tHandle, theItem, theText, itemTags) self._scanActive(tHandle, theItem, theText, itemTags)
# Update timestamps for index changes # Update timestamps for index changes
nowTime = round(time()) nowTime = time()
self._indexChange = nowTime self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime self._rootChange[theItem.itemRoot] = nowTime
@@ -737,8 +749,8 @@ class ItemIndex:
IndexHeading object for each header of the text. IndexHeading object for each header of the text.
""" """
def __init__(self, theProject): def __init__(self, project):
self.theProject = theProject self._project = project
self._items = {} self._items = {}
return return
@@ -802,7 +814,7 @@ class ItemIndex:
"""Iterate over all items and headers in the novel structure for """Iterate over all items and headers in the novel structure for
a given root handle, or for all if root handle is None. a given root handle, or for all if root handle is None.
""" """
for tItem in self.theProject.tree: for tItem in self._project.tree:
if tItem is None: if tItem is None:
continue continue
if tItem.isNoteLayout(): if tItem.isNoteLayout():
@@ -885,7 +897,7 @@ class ItemIndex:
if not isHandle(tHandle): if not isHandle(tHandle):
raise ValueError("itemIndex keys must be handles") raise ValueError("itemIndex keys must be handles")
nwItem = self.theProject.tree[tHandle] nwItem = self._project.tree[tHandle]
if nwItem is not None: if nwItem is not None:
tItem = IndexItem(tHandle, nwItem) tItem = IndexItem(tHandle, nwItem)
tItem.unpackData(tData) tItem.unpackData(tData)
+4 -1
View File
@@ -369,8 +369,11 @@ class NWProject(QObject):
self._scanProjectFolder() self._scanProjectFolder()
self._index.loadIndex() self._index.loadIndex()
self.updateWordCounts() if xmlReader.state == XMLReadState.WAS_LEGACY:
# Often, the index needs to be rebuilt when updating format
self._index.rebuildIndex()
self.updateWordCounts()
self._projOpened = time() self._projOpened = time()
self._projAltered = False self._projAltered = False
+9 -14
View File
@@ -331,20 +331,15 @@ class NWTree:
def setOrder(self, newOrder): def setOrder(self, newOrder):
"""Reorders the tree based on a list of items. """Reorders the tree based on a list of items.
""" """
tmpOrder = [] tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
# Add all known elements to a new temp list # Something is wrong, so let's debug it
for tHandle in newOrder: for tHandle in newOrder:
if tHandle in self._projTree: if tHandle not in self._projTree:
tmpOrder.append(tHandle) logger.error("Handle '%s' in new tree order is not in old order", tHandle)
else: for tHandle in self._treeOrder:
logger.error("Handle '%s' in new tree order is not in project tree", tHandle) if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
# Do a reverse lookup to check for items that will be lost
# This is mainly for debugging purposes
for tHandle in self._treeOrder:
if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
# Save the temp list # Save the temp list
self._treeOrder = tmpOrder self._treeOrder = tmpOrder
+2 -11
View File
@@ -833,17 +833,8 @@ class GuiMain(QMainWindow):
tStart = time() tStart = time()
self.projView.saveProjectTasks() self.projView.saveProjectTasks()
self.theProject.index.clearIndex() self.theProject.index.rebuildIndex()
self.projView.populateTree()
for tItem in self.theProject.tree:
if tItem is None: # pragma: no cover
continue # This is a bug trap
logger.debug("Indexing '%s'", tItem.itemName)
if self.theProject.index.reIndexHandle(tItem.itemHandle):
# Update Word Counts
self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
self.projView.setTreeItemValues(tItem.itemHandle)
tEnd = time() tEnd = time()
self.setStatus( self.setStatus(
+8 -1
View File
@@ -90,7 +90,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
assert theIndex._tagsIndex._tags == {} assert theIndex._tagsIndex._tags == {}
assert theIndex._itemIndex._items == {} assert theIndex._itemIndex._items == {}
# No folder for sloading # No folder for loading
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None) mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
assert theIndex.loadIndex() is False assert theIndex.loadIndex() is False
@@ -108,6 +108,13 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
assert str(theIndex._tagsIndex.packData()) == tagIndex assert str(theIndex._tagsIndex.packData()) == tagIndex
assert str(theIndex._itemIndex.packData()) == itemsIndex assert str(theIndex._itemIndex.packData()) == itemsIndex
# Rebuild index
theIndex.clearIndex()
theIndex.rebuildIndex()
assert str(theIndex._tagsIndex.packData()) == tagIndex
assert str(theIndex._itemIndex.packData()) == itemsIndex
# Check File # Check File
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
+13 -1
View File
@@ -230,7 +230,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1] assert "The file format of your project is about to be" in mockGUI.lastQuestion[1]
mockGUI.askResponse = True mockGUI.askResponse = True
# Won't convert legacy file # Won't open project from newer version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999))
mockGUI.askResponse = False mockGUI.askResponse = False
@@ -245,6 +245,18 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
assert theProject.closeProject() assert theProject.closeProject()
# Trigger an index rebuild
with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True)
mockGUI.askResponse = True
theProject.index._indexBroken = True
assert theProject.openProject(fncPath) is True
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1]
assert theProject.index._indexBroken is False
assert theProject.closeProject()
# END Test testCoreProject_Open # END Test testCoreProject_Open
+48 -1
View File
@@ -24,6 +24,7 @@ import random
from pathlib import Path from pathlib import Path
from mock import causeOSError
from tools import readFile from tools import readFile
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -230,6 +231,36 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# END Test testCoreTree_BuildTree # END Test testCoreTree_BuildTree
@pytest.mark.core
def testCoreTree_PackUnpack(mockGUI, mockItems):
"""Test packing and unpacking data.
"""
theProject = NWProject(mockGUI)
theTree = NWTree(theProject)
aHandles = []
for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle)
theTree.append(tHandle, pHandle, nwItem)
theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
# Pack
tree = theTree.pack()
for i, (tHandle, pHandle, nwItem) in enumerate(mockItems):
assert tree[i]["itemAttr"]["handle"] == tHandle
# Unpack
theTree.clear()
assert len(theTree) == 0
assert theTree.handles() == []
assert theTree.unpack(tree) is True
assert theTree.handles() == aHandles
# END Test testCoreTree_PackUnpack
@pytest.mark.core @pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems): def testCoreTree_Methods(mockGUI, mockItems):
"""Test various class methods. """Test various class methods.
@@ -272,6 +303,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
# Iter roots
roots = list(theTree.iterRoots(None))
assert roots[0][0] == "a000000000001"
assert roots[1][0] == "a000000000002"
assert roots[2][0] == "a000000000003"
assert roots[3][0] == "a000000000004"
# Add a fake item to root and check that it can handle it # Add a fake item to root and check that it can handle it
theTree._treeRoots["0000000000000"] = NWItem(theProject) theTree._treeRoots["0000000000000"] = NWItem(theProject)
assert theTree.findRoot(nwItemClass.WORLD) is None assert theTree.findRoot(nwItemClass.WORLD) is None
@@ -363,7 +401,7 @@ def testCoreTree_Stats(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_Reorder(mockGUI, mockItems): def testCoreTree_Reorder(caplog, mockGUI, mockItems):
"""Test changing tree order. """Test changing tree order.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -384,12 +422,16 @@ def testCoreTree_Reorder(mockGUI, mockItems):
theTree.setOrder(bHandle) theTree.setOrder(bHandle)
assert theTree.handles() == bHandle assert theTree.handles() == bHandle
caplog.clear()
theTree.setOrder(bHandle + ["stuff"]) theTree.setOrder(bHandle + ["stuff"])
assert theTree.handles() == bHandle assert theTree.handles() == bHandle
assert "Handle 'stuff' in new tree order is not in old order" in caplog.text
caplog.clear()
theTree._treeOrder.append("stuff") theTree._treeOrder.append("stuff")
theTree.setOrder(bHandle) theTree.setOrder(bHandle)
assert theTree.handles() == bHandle assert theTree.handles() == bHandle
assert "Handle 'stuff' in old tree order is not in new order" in caplog.text
# END Test testCoreTree_Reorder # END Test testCoreTree_Reorder
@@ -421,6 +463,11 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpPath):
theProject._storage._runtimePath = None theProject._storage._runtimePath = None
assert theTree.writeToCFile() is False assert theTree.writeToCFile() is False
theProject._storage._runtimePath = tmpPath
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert theTree.writeToCFile() is False
theProject._storage._runtimePath = tmpPath theProject._storage._runtimePath = tmpPath
assert theTree.writeToCFile() is True assert theTree.writeToCFile() is True