Clean up the index code a bit and add index validation

This commit is contained in:
Veronica Berglyd Olsen
2022-05-29 00:18:48 +02:00
parent 94ba7a2f94
commit 347d34bf83
3 changed files with 97 additions and 67 deletions
+92 -62
View File
@@ -34,7 +34,9 @@ from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
from novelwriter.common import checkInt, jsonEncode from novelwriter.common import (
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -74,13 +76,11 @@ class NWIndex():
def clearIndex(self): def clearIndex(self):
"""Clear the index dictionaries and time stamps. """Clear the index dictionaries and time stamps.
""" """
self._tags = {}
self._items = {}
self._timeNovel = 0 self._timeNovel = 0
self._timeNotes = 0 self._timeNotes = 0
self._timeIndex = 0 self._timeIndex = 0
self._tags = {}
self._items = {}
return return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
@@ -90,10 +90,8 @@ class NWIndex():
return return
logger.debug("Removing item '%s' from the index", tHandle) logger.debug("Removing item '%s' from the index", tHandle)
for tTag in self._items[tHandle].allTags(): for tTag in self._items[tHandle].allTags():
self._tags.pop(tTag, None) self._tags.pop(tTag, None)
self._items.pop(tHandle, None) self._items.pop(tHandle, None)
return return
@@ -144,30 +142,36 @@ class NWIndex():
try: try:
with open(indexFile, mode="r", encoding="utf-8") as inFile: with open(indexFile, mode="r", encoding="utf-8") as inFile:
theData = json.load(inFile) theData = json.load(inFile)
except Exception: except Exception:
logger.error("Failed to load index file") logger.error("Failed to load index file")
logException() logException()
self._indexBroken = True self._indexBroken = True
return False return False
self._tags = theData.get("tagsIndex", {}) try:
for tHandle, tData in theData.get("itemIndex", {}).items(): self._validateTagsIndex(theData["tagsIndex"])
nwItem = self.theProject.tree[tHandle] self._validateItemIndex(theData["itemIndex"])
if nwItem is not None: except Exception:
tItem = IndexItem(tHandle, nwItem) logger.error("The index content is invalid")
tItem.unpackData(tData) logException()
self._items[tHandle] = tItem self._indexBroken = True
return False
nowTime = round(time()) logger.debug("Checking index")
self._timeNovel = nowTime
self._timeNotes = nowTime # Check that all files are indexed
self._timeIndex = nowTime for fHandle in self.theProject.projFiles:
if fHandle not in self._items:
logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle)
nowTime = round(time())
self._timeNovel = nowTime
self._timeNotes = nowTime
self._timeIndex = nowTime
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
self._checkIndex()
return True return True
def saveIndex(self): def saveIndex(self):
@@ -214,11 +218,11 @@ class NWIndex():
logger.info("Not indexing non-file item '%s'", tHandle) logger.info("Not indexing non-file item '%s'", tHandle)
return False return False
# Delete the old entry and create a new
self.deleteHandle(tHandle) self.deleteHandle(tHandle)
# Run word counter for the whole text
self._items[tHandle] = IndexItem(tHandle, theItem) self._items[tHandle] = IndexItem(tHandle, theItem)
# Run word counter for the whole text
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
theItem.setCharCount(cC) theItem.setCharCount(cC)
theItem.setWordCount(wC) theItem.setWordCount(wC)
@@ -249,7 +253,7 @@ class NWIndex():
continue continue
if aLine.startswith("#"): if aLine.startswith("#"):
isTitle = self._indexTitle(tHandle, aLine, nLine, itemLayout) isTitle = self._indexTitle(tHandle, aLine, nLine)
if isTitle and nLine > 0: if isTitle and nLine > 0:
if nTitle > 0: if nTitle > 0:
lastText = "\n".join(theLines[nTitle-1:nLine-1]) lastText = "\n".join(theLines[nTitle-1:nLine-1])
@@ -257,7 +261,7 @@ class NWIndex():
nTitle = nLine nTitle = nLine
elif aLine.startswith("@"): elif aLine.startswith("@"):
self._indexKeyword(tHandle, aLine, nLine, nTitle, itemClass) self._indexKeyword(tHandle, aLine, nTitle, itemClass)
elif aLine.startswith("%"): elif aLine.startswith("%"):
if nTitle > 0: if nTitle > 0:
@@ -274,7 +278,7 @@ class NWIndex():
lastText = "\n".join(theLines[nTitle-1:]) lastText = "\n".join(theLines[nTitle-1:])
self._indexWordCounts(tHandle, lastText, nTitle) self._indexWordCounts(tHandle, lastText, nTitle)
# Index page with no titles and references # Also count words on a page with no titles
if nTitle == 0: if nTitle == 0:
self._indexWordCounts(tHandle, theText, nTitle) self._indexWordCounts(tHandle, theText, nTitle)
@@ -292,7 +296,7 @@ class NWIndex():
# Internal Indexers # Internal Indexers
## ##
def _indexTitle(self, tHandle, aLine, nLine, itemLayout): def _indexTitle(self, tHandle, aLine, nLine):
"""Save information about the title and its location in the """Save information about the title and its location in the
file to the index. file to the index.
""" """
@@ -341,7 +345,7 @@ class NWIndex():
self._items[tHandle].setHeadingSynopsis(sTitle, theText) self._items[tHandle].setHeadingSynopsis(sTitle, theText)
return return
def _indexKeyword(self, tHandle, aLine, nLine, nTitle, itemClass): def _indexKeyword(self, tHandle, aLine, nTitle, itemClass):
"""Validate and save the information about a reference to a tag """Validate and save the information about a reference to a tag
in another file. in another file.
""" """
@@ -361,11 +365,9 @@ class NWIndex():
"heading": sTitle, "heading": sTitle,
"class": itemClass.name, "class": itemClass.name,
} }
if tHandle in self._items: self._items[tHandle].setHeadingTag(sTitle, theBits[1])
self._items[tHandle].setHeadingTag(sTitle, theBits[1])
else: else:
if tHandle in self._items: self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0])
self._items[tHandle].addHeadingReferences(sTitle, theBits[1:], theBits[0])
return return
@@ -627,26 +629,51 @@ class NWIndex():
return theHandles return theHandles
## def _validateTagsIndex(self, tagsIndex):
# Index Checkers """Iterate through the tagsIndex loaded from cache and check
## that it's valid.
def _checkIndex(self):
"""Check that the entries in the index are valid and contain the
elements it should. Also check that each file present in the
contents folder when the project was loaded are also present in
the fileMeta index.
""" """
logger.debug("Checking index") self._tags = {}
tStart = time() if not isinstance(tagsIndex, dict):
raise ValueError("tagsIndex is not a dict")
# If the index was ok, we check that project files are indexed for tagKey, tagData in tagsIndex.items():
for fHandle in self.theProject.projFiles: if not isinstance(tagKey, str):
if fHandle not in self._items: raise ValueError("tagsIndex keys must be a strings")
logger.warning("Item '%s' is not in the index", fHandle) if "handle" not in tagData:
self.reIndexHandle(fHandle) raise KeyError("A tagIndex item is missing a handle entry")
if "heading" not in tagData:
raise KeyError("A tagIndex item is missing a heading entry")
if "class" not in tagData:
raise KeyError("A tagIndex item is missing a class entry")
if not isHandle(tagData["handle"]):
raise ValueError("tagsIndex handle must be a handle")
if not isTitleTag(tagData["heading"]):
raise ValueError("tagsIndex heading must be a title tag")
if not isItemClass(tagData["class"]):
raise ValueError("tagsIndex handle must be an nwItemClass")
logger.verbose("Index check completed in %.3f ms", (time() - tStart)*1000) self._tags = tagsIndex
return
def _validateItemIndex(self, itemIndex):
"""Iterate through the itemIndex loaded from cache and check
that it's valid.
"""
self._items = {}
if not isinstance(itemIndex, dict):
raise ValueError("itemIndex is not a dict")
for tHandle, tData in itemIndex.items():
if not isHandle(tHandle):
raise ValueError("itemIndex keys must be handles")
nwItem = self.theProject.tree[tHandle]
if nwItem is not None:
tItem = IndexItem(tHandle, nwItem)
tItem.unpackData(tData)
self._items[tHandle] = tItem
return return
@@ -734,6 +761,10 @@ def countWords(theText):
return charCount, wordCount, paraCount return charCount, wordCount, paraCount
# =============================================================================================== #
# Indexer Objects
# =============================================================================================== #
class IndexItem: class IndexItem:
def __init__(self, tHandle, tItem): def __init__(self, tHandle, tItem):
@@ -773,20 +804,11 @@ class IndexItem:
# Setters # Setters
## ##
def setLevel(self, level):
if level in H_VALID:
self._level = level
else:
self._level = "H0"
return
def updateLevel(self, level): def updateLevel(self, level):
"""Set the level only if it is H0. """Set the level only if it is H0.
""" """
if level in H_VALID and self._level == "H0": if self._level == "H0":
self._level = level self._level = level
else:
self._level = "H0"
return return
def addHeading(self, tHeading): def addHeading(self, tHeading):
@@ -867,6 +889,8 @@ class IndexItem:
self._level = data.get("level", "H0") self._level = data.get("level", "H0")
references = data.get("references", {}) references = data.get("references", {})
for sTitle, hData in data.get("headings", {}).items(): for sTitle, hData in data.get("headings", {}).items():
if not isTitleTag(sTitle):
raise ValueError("The itemIndex contains an invalid title key")
tHeading = IndexHeading(sTitle) tHeading = IndexHeading(sTitle)
tHeading.unpackData(hData) tHeading.unpackData(hData)
tHeading.unpackReferences(references.get(sTitle, {})) tHeading.unpackReferences(references.get(sTitle, {}))
@@ -940,8 +964,6 @@ class IndexHeading:
def setLevel(self, level): def setLevel(self, level):
if level in H_VALID: if level in H_VALID:
self._level = level self._level = level
else:
self._level = "H0"
return return
def setCounts(self, charCount, wordCount, paraCount): def setCounts(self, charCount, wordCount, paraCount):
@@ -1007,7 +1029,15 @@ class IndexHeading:
"""Unpack a set of references from a dictionary. """Unpack a set of references from a dictionary.
""" """
for tagKey, refTypes in data.items(): for tagKey, refTypes in data.items():
self._refs[tagKey] = set(refTypes) if not isinstance(tagKey, str):
raise ValueError("itemIndex reference key must be a string")
if not isinstance(refTypes, list):
raise ValueError("itemIndex reference types must be a list")
for refType in refTypes:
if refType in nwKeyWords.VALID_KEYS:
self.addReference(tagKey, refType)
else:
raise ValueError("The itemIndex contains an invalid reference type")
return return
# END Class IndexHeading # END Class IndexHeading
+3 -3
View File
@@ -123,15 +123,15 @@ class NWProject():
## ##
@property @property
def index(self) -> NWIndex: def index(self):
return self._projIndex return self._projIndex
@property @property
def tree(self) -> NWTree: def tree(self):
return self._projTree return self._projTree
@property @property
def options(self) -> OptionState: def options(self):
return self._optState return self._optState
## ##
@@ -39,7 +39,7 @@
} }
}, },
"88243afbe5ed8": { "88243afbe5ed8": {
"level": "H0", "level": "H3",
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, "T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} "T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
@@ -49,7 +49,7 @@
} }
}, },
"f96ec11c6a3da": { "f96ec11c6a3da": {
"level": "H0", "level": "H3",
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, "T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} "T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}