From 04198f3ede4449f69f4464efdd549a26aab4f078 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:13:08 +0200 Subject: [PATCH 1/4] Remove quiet flag from index rebuild --- novelwriter/guimain.py | 8 ++++---- tests/test_gui/test_gui_guimain.py | 2 +- tests/test_gui/test_gui_statusbar.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 91d7ff59..ac065b03 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -497,7 +497,8 @@ class GuiMain(QMainWindow): # Check if we need to rebuild the index if SHARED.project.index.indexBroken: - SHARED.info(self.tr("The project index is outdated or broken. Rebuilding index.")) + if not SHARED.project.index.indexUpgrade: + SHARED.info(self.tr("The project index is broken. Rebuilding index.")) self.rebuildIndex() # Make sure the changed status is set to false on things opened @@ -729,7 +730,7 @@ class GuiMain(QMainWindow): return - def rebuildIndex(self, beQuiet: bool = False) -> None: + def rebuildIndex(self) -> None: """Rebuild the entire index.""" if SHARED.hasProject: logger.info("Rebuilding index ...") @@ -746,8 +747,7 @@ class GuiMain(QMainWindow): self._updateStatusWordCount() QApplication.restoreOverrideCursor() - if not beQuiet: - SHARED.info(self.tr("The project index has been successfully rebuilt.")) + SHARED.info(self.tr("The project index has been successfully rebuilt.")) ## # Main Dialogs diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 258d5e0a..092a0486 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -713,7 +713,7 @@ def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): cHandle = SHARED.project.newFile("Jane", C.hCharRoot) newDoc = SHARED.project.storage.getDocument(cHandle) newDoc.writeDocument("# Jane\n\n@tag: Jane\n\n") - nwGUI.rebuildIndex(beQuiet=True) + nwGUI.rebuildIndex() assert SHARED.focusMode is False diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 19c3f277..8bbb6fbe 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): cHandle = SHARED.project.newFile("A Note", C.hCharRoot) newDoc = SHARED.project.storage.getDocument(cHandle) newDoc.writeDocument("# A Note\n\n") - nwGUI.rebuildIndex(beQuiet=True) + nwGUI.rebuildIndex() status = nwGUI.mainStatus From 0344701f1b3c99eb1b43e448fe8089c735dc2bd8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:13:40 +0200 Subject: [PATCH 2/4] Add helper function to combine JSON strings as larger JSON file --- novelwriter/common.py | 6 ++++++ tests/test_base/test_base_common.py | 19 +++++++++++++++---- tests/tools.py | 4 ++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 8fd9386a..5bd35d50 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -560,6 +560,12 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: return "".join(buffer) +def jsonCombine(data: dict[str, str]) -> str: + """Combine multiple already packed JSON strings.""" + payload = ",\n".join(f' "{k}": {v}' for k, v in data.items()) + return f"{{\n{payload}\n}}\n" + + ## # XML Helpers ## diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 95d339df..7ba2ddb0 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -36,10 +36,10 @@ from novelwriter.common import ( describeFont, elide, encodeMimeHandles, firstFloat, fontMatcher, formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, - isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, - minmax, numberToRoman, openExternalPath, processDialogSymbols, - readTextFile, simplified, transferCase, uniqueCompact, utf16CharMap, - xmlElement, xmlIndent, xmlSubElem, yesNo + isItemType, isListInstance, isTitleTag, jsonCombine, jsonEncode, + makeFileNameSafe, minmax, numberToRoman, openExternalPath, + processDialogSymbols, readTextFile, simplified, transferCase, + uniqueCompact, utf16CharMap, xmlElement, xmlIndent, xmlSubElem, yesNo ) from novelwriter.enum import nwItemClass @@ -651,6 +651,17 @@ def testBaseCommon_jsonEncode(): ) +@pytest.mark.base +def testBaseCommon_jsonCombine(): + """Test the jsonCombine function.""" + assert jsonCombine({"a": "[1, 2]", "b": "[3, 4]"}) == ( + '{\n' + ' "a": [1, 2],\n' + ' "b": [3, 4]\n' + '}\n' + ) + + @pytest.mark.base def testBaseCommon_xmlIndent(): """Test the xmlIndent function.""" diff --git a/tests/tools.py b/tests/tools.py index 9eec20d0..5d0b7c84 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -66,8 +66,8 @@ class C: def cmpFiles( fileOne: str | Path, fileTwo: str | Path, - ignoreLines: list | None = None, - ignoreStart: tuple | None = None + ignoreLines: list[int] | None = None, + ignoreStart: tuple[str] | None = None ) -> bool: """Compare two files, with optional line ignore.""" if ignoreLines is None: From 850976335c4d8bd58285f309c80f0768936ec09c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:18:42 +0200 Subject: [PATCH 3/4] Don't show index rebuild dialog when just upgrading index --- novelwriter/core/index.py | 35 +++++++++++++------ .../coreIndex_LoadSave_tagsIndex.json | 4 +++ tests/test_core/test_core_index.py | 2 +- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 2a9aba3a..d1955c45 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -33,8 +33,10 @@ from pathlib import Path from time import time from typing import TYPE_CHECKING -from novelwriter import SHARED -from novelwriter.common import isHandle, isItemClass, isTitleTag, jsonEncode +from novelwriter import SHARED, __hexversion__ +from novelwriter.common import ( + formatTimeStamp, isHandle, isItemClass, isTitleTag, jsonCombine, jsonEncode +) from novelwriter.constants import nwFiles, nwKeyWords, nwStyles from novelwriter.core.indexdata import NOTE_TYPES, TT_NONE, IndexHeading, IndexNode, T_NoteTypes from novelwriter.core.novelmodel import NovelModel @@ -82,6 +84,11 @@ class Index: a rebuild of the index data. """ + __slots__ = ( + "_indexBroken", "_indexChange", "_indexUpgrade", "_itemIndex", "_novelExtra", + "_novelModels", "_project", "_rootChange", "_tagsIndex", + ) + def __init__(self, project: NWProject) -> None: self._project = project @@ -90,6 +97,7 @@ class Index: self._tagsIndex = TagsIndex() self._itemIndex = ItemIndex(project, self._tagsIndex) self._indexBroken = False + self._indexUpgrade = False # Models self._novelModels: dict[str, NovelModel] = {} @@ -110,6 +118,10 @@ class Index: def indexBroken(self) -> bool: return self._indexBroken + @property + def indexUpgrade(self) -> bool: + return self._indexUpgrade + ## # Getters ## @@ -241,6 +253,8 @@ class Index: return False try: + meta = data.get("novelWriter.meta", {}) + self._indexUpgrade = meta.get("version") != __hexversion__ self._tagsIndex.unpackData(data["novelWriter.tagsIndex"]) self._itemIndex.unpackData(data["novelWriter.itemIndex"]) except Exception: @@ -273,23 +287,22 @@ class Index: return False logger.debug("Saving index file") - tStart = time() + start = time() try: - tagsIndex = jsonEncode(self._tagsIndex.packData(), n=1, nmax=2) - itemIndex = jsonEncode(self._itemIndex.packData(), n=1, nmax=4) + meta = {"version": __hexversion__, "timestamp": formatTimeStamp(start)} with open(indexFile, mode="w+", encoding="utf-8") as outFile: - outFile.write("{\n") - outFile.write(f' "novelWriter.tagsIndex": {tagsIndex},\n') - outFile.write(f' "novelWriter.itemIndex": {itemIndex}\n') - outFile.write("}\n") - + outFile.write(jsonCombine({ + "novelWriter.meta": jsonEncode(meta, n=1), + "novelWriter.tagsIndex": jsonEncode(self._tagsIndex.packData(), n=1, nmax=2), + "novelWriter.itemIndex": jsonEncode(self._itemIndex.packData(), n=1, nmax=4), + })) except Exception: logger.error("Failed to save index file") logException() return False - logger.debug("Index saved in %.3f ms", (time() - tStart)*1000) + logger.debug("Index saved in %.3f ms", (time() - start)*1000) return True diff --git a/tests/reference/coreIndex_LoadSave_tagsIndex.json b/tests/reference/coreIndex_LoadSave_tagsIndex.json index c8365601..af9474f0 100644 --- a/tests/reference/coreIndex_LoadSave_tagsIndex.json +++ b/tests/reference/coreIndex_LoadSave_tagsIndex.json @@ -1,4 +1,8 @@ { + "novelWriter.meta": { + "version": "0x020800a2", + "timestamp": "2025-10-05 17:06:58" + }, "novelWriter.tagsIndex": { "bod": {"name": "Bod", "display": "Nobody Owens", "handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"}, "main": {"name": "Main", "display": "Main", "handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"}, diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 332c27c3..9032aed8 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -157,7 +157,7 @@ def testCoreIndex_LoadSave(qtbot, monkeypatch, prjLipsum, nwGUI, tstPaths): # Check File copyfile(projFile, testFile) - assert cmpFiles(testFile, compFile) + assert cmpFiles(testFile, compFile, ignoreLines=[3, 4]) # Write an empty index file and load it projFile.write_text("{}", encoding="utf-8") From 050e3b09ad46c16703420e53363e1d6cdfac9f8c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:39:48 +0200 Subject: [PATCH 4/4] Fix test coverage --- novelwriter/guimain.py | 2 +- tests/test_gui/test_gui_guimain.py | 7 ++++--- tests/tools.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ac065b03..acf6114b 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -498,7 +498,7 @@ class GuiMain(QMainWindow): # Check if we need to rebuild the index if SHARED.project.index.indexBroken: if not SHARED.project.index.indexUpgrade: - SHARED.info(self.tr("The project index is broken. Rebuilding index.")) + SHARED.warn(self.tr("The project index is broken. Rebuilding index.")) self.rebuildIndex() # Make sure the changed status is set to false on things opened diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 092a0486..0e15b71e 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -32,7 +32,8 @@ from PyQt6.QtCore import Qt from PyQt6.QtGui import QPalette from PyQt6.QtWidgets import QInputDialog, QMessageBox -from novelwriter import CONFIG, SHARED +from novelwriter import CONFIG, SHARED, __hexversion__ +from novelwriter.common import jsonEncode from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT from novelwriter.constants import nwFiles from novelwriter.dialogs.editlabel import GuiEditLabel @@ -825,11 +826,11 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) nwGUI.viewDocument(C.hTitlePage) # Handle broken index on project open + idxData = jsonEncode({"novelWriter.meta": {"version": __hexversion__}}) nwGUI.closeProject() idxPath: Path = projPath / "meta" / nwFiles.INDEX_FILE assert idxPath.read_text(encoding="utf-8") != "{}" - idxPath.write_text("{}", encoding="utf-8") - assert idxPath.read_text(encoding="utf-8") == "{}" + idxPath.write_text(idxData, encoding="utf-8") nwGUI.openProject(projPath) nwGUI.saveProject() diff --git a/tests/tools.py b/tests/tools.py index 5d0b7c84..bca68899 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -67,7 +67,7 @@ def cmpFiles( fileOne: str | Path, fileTwo: str | Path, ignoreLines: list[int] | None = None, - ignoreStart: tuple[str] | None = None + ignoreStart: tuple | None = None ) -> bool: """Compare two files, with optional line ignore.""" if ignoreLines is None: