Complete annotations in project class and update tests

This commit is contained in:
Veronica Berglyd Olsen
2023-07-22 01:06:34 +02:00
parent d382775d1d
commit c8d588d6d2
5 changed files with 96 additions and 93 deletions
+3 -4
View File
@@ -77,7 +77,7 @@ class OptionState:
the Config instead. the Config instead.
""" """
def __init__(self, project: NWProject): def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
self._state = {} self._state = {}
return return
@@ -87,8 +87,7 @@ class OptionState:
## ##
def loadSettings(self) -> bool: def loadSettings(self) -> bool:
"""Load the options dictionary from the project settings file. """Load the options dictionary from the project."""
"""
stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path): if not isinstance(stateFile, Path):
return False return False
@@ -116,7 +115,7 @@ class OptionState:
return True return True
def saveSettings(self) -> bool: def saveSettings(self) -> bool:
"""Save the options dictionary to the project settings file.""" """Save the options dictionary to the project."""
stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE) stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path): if not isinstance(stateFile, Path):
return False return False
+68 -68
View File
@@ -28,6 +28,7 @@ import json
import logging import logging
from time import time from time import time
from typing import TYPE_CHECKING, Iterator
from pathlib import Path from pathlib import Path
from functools import partial from functools import partial
@@ -48,6 +49,11 @@ from novelwriter.common import (
checkStringNone, formatTimeStamp, hexToInt, makeFileNameSafe, minmax checkStringNone, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
) )
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
from novelwriter.core.item import NWItem
from novelwriter.core.status import NWStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,7 +61,7 @@ class NWProject(QObject):
projectStatusChanged = pyqtSignal(bool) projectStatusChanged = pyqtSignal(bool)
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
# Internal # Internal
@@ -69,19 +75,14 @@ class NWProject(QObject):
self._index = NWIndex(self) # The project index self._index = NWIndex(self) # The project index
self._session = NWSessionLog(self) # The session record self._session = NWSessionLog(self) # The session record
# Data Cache
self._langData = {} # Localisation data
# Project Status # Project Status
self._langData = {} # Localisation data
self._projChanged = False # The project has unsaved changes self._projChanged = False # The project has unsaved changes
self._lockedBy = None # Data on which computer has the project open self._lockedBy = None # Data on which computer has the project open
# Internal Mapping # Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
# Set Defaults
self.clearProject()
return return
## ##
@@ -89,23 +90,23 @@ class NWProject(QObject):
## ##
@property @property
def options(self): def options(self) -> OptionState:
return self._options return self._options
@property @property
def storage(self): def storage(self) -> NWStorage:
return self._storage return self._storage
@property @property
def data(self): def data(self) -> NWProjectData:
return self._data return self._data
@property @property
def tree(self): def tree(self) -> NWTree:
return self._tree return self._tree
@property @property
def index(self): def index(self) -> NWIndex:
return self._index return self._index
@property @property
@@ -113,11 +114,11 @@ class NWProject(QObject):
return self._session return self._session
@property @property
def projOpened(self): def projOpened(self) -> float:
return self._session.start return self._session.start
@property @property
def projChanged(self): def projChanged(self) -> bool:
return self._projChanged return self._projChanged
## ##
@@ -139,7 +140,7 @@ class NWProject(QObject):
"""Add a new file with a given label and parent item.""" """Add a new file with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FILE) return self._tree.create(label, parent, nwItemType.FILE)
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""): def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, addText: str = "") -> bool:
"""Write content to a new document after it is created. This """Write content to a new document after it is created. This
will not run if the file exists and is not empty. will not run if the file exists and is not empty.
""" """
@@ -165,7 +166,7 @@ class NWProject(QObject):
return True return True
def removeItem(self, tHandle): def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the """Remove an item from the project. This will delete both the
project entry and a document file if it exists. project entry and a document file if it exists.
""" """
@@ -182,7 +183,7 @@ class NWProject(QObject):
return True return True
def trashFolder(self): def trashFolder(self) -> str:
"""Add the special trash root folder to the project.""" """Add the special trash root folder to the project."""
trashHandle = self._tree.trashRoot() trashHandle = self._tree.trashRoot()
if trashHandle is None: if trashHandle is None:
@@ -194,23 +195,28 @@ class NWProject(QObject):
# Project Methods # Project Methods
## ##
def clearProject(self): def clearProject(self) -> None:
"""Clear the data for the current project, and set them to """Clear the data for the current project, and set them to
default values. default values.
"""
# Project Status
self._projChanged = False
# Project Tree Note: Don't clear the lockedBy data here as it is needed after
this function is called.
"""
# Core Elements
self._options = OptionState(self)
self._storage.clear() self._storage.clear()
self._data = NWProjectData(self)
self._tree.clear() self._tree.clear()
self._index.clearIndex() self._index.clearIndex()
self._data = NWProjectData(self)
self._session = NWSessionLog(self) self._session = NWSessionLog(self)
# Project Status
self._langData = {}
self._projChanged = False
return return
def openProject(self, projPath, overrideLock=False): def openProject(self, projPath: str | Path, overrideLock: bool = False) -> bool:
"""Open the project file provided. If it 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, 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 parse the XML of the file and populate the project variables and
@@ -342,7 +348,7 @@ class NWProject(QObject):
return True return True
def saveProject(self, autoSave=False): def saveProject(self, autoSave: bool = False) -> bool:
"""Save the project main XML file. The saving command itself """Save the project main XML file. The saving command itself
uses a temporary filename, and the file is replaced afterwards uses a temporary filename, and the file is replaced afterwards
to make sure if the save fails, we're not left with a truncated to make sure if the save fails, we're not left with a truncated
@@ -395,9 +401,8 @@ class NWProject(QObject):
return True return True
def closeProject(self, idleTime=0.0): def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the current project and clear all meta data. """Close the current project and clear all meta data."""
"""
logger.info("Closing project") logger.info("Closing project")
self._options.saveSettings() self._options.saveSettings()
self._tree.writeToCFile() self._tree.writeToCFile()
@@ -406,11 +411,10 @@ class NWProject(QObject):
self._storage.closeSession() self._storage.closeSession()
self.clearProject() self.clearProject()
self._lockedBy = None self._lockedBy = None
return True return
def backupProject(self, doNotify): def backupProject(self, doNotify: bool) -> bool:
"""Create a zip file of the entire project. """Create a zip file of the entire project."""
"""
if not self._storage.isOpen(): if not self._storage.isOpen():
logger.error("No project open") logger.error("No project open")
return False return False
@@ -467,9 +471,8 @@ class NWProject(QObject):
# Setters # Setters
## ##
def setDefaultStatusImport(self): def setDefaultStatusImport(self) -> None:
"""Set the default status and importance values. """Set the default status and importance values."""
"""
self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100)) self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0)) self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0)) self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
@@ -480,43 +483,40 @@ class NWProject(QObject):
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0)) self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
return return
def setProjectLang(self, theLang): def setProjectLang(self, language: str | None) -> None:
"""Set the project-specific language. """Set the project-specific language."""
""" language = checkStringNone(language, None)
theLang = checkStringNone(theLang, None) if self._data.language != language:
if self._data.language != theLang: self._data.setLanguage(language)
self._data.setLanguage(theLang)
self._loadProjectLocalisation() self._loadProjectLocalisation()
self.setProjectChanged(True) self.setProjectChanged(True)
return True return
def setTreeOrder(self, newOrder): def setTreeOrder(self, order: list[str]) -> None:
"""A list representing the linear/flattened order of project """A list representing the linear/flattened order of project
items in the GUI project tree. The user can rearrange the order items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class. by drag-and-drop. Forwarded to the NWTree class.
""" """
if len(self._tree) != len(newOrder): if len(self._tree) != len(order):
logger.warning("Sizes of new and old tree order do not match") logger.warning("Sizes of new and old tree order do not match")
self._tree.setOrder(newOrder) self._tree.setOrder(order)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return
def setStatusColours(self, newCols, delCols): def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool:
"""Update the list of novel file status flags. """Update the list of novel file status flags."""
""" return self._setStatusImport(new, deleted, self._data.itemStatus)
return self._setStatusImport(newCols, delCols, self._data.itemStatus)
def setImportColours(self, newCols, delCols): def setImportColours(self, new: list[dict], deleted: list[str]) -> bool:
"""Update the list of note file importance flags. """Update the list of note file importance flags."""
""" return self._setStatusImport(new, deleted, self._data.itemImport)
return self._setStatusImport(newCols, delCols, self._data.itemImport)
def setProjectChanged(self, value): def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the """Toggle the project changed flag, and propagate the
information to the GUI statusbar. information to the GUI statusbar.
""" """
if isinstance(value, bool): if isinstance(status, bool):
self._projChanged = value self._projChanged = status
self.projectStatusChanged.emit(self._projChanged) self.projectStatusChanged.emit(self._projChanged)
return self._projChanged return self._projChanged
@@ -536,7 +536,7 @@ class NWProject(QObject):
""" """
return self._data.editTime + round(time() - self._session.start) return self._data.editTime + round(time() - self._session.start)
def getProjectItems(self): def getProjectItems(self) -> Iterator[NWItem]:
"""This function ensures that the item tree loaded is sent to """This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle, is, the parent item must be sent before its child. In principle,
@@ -577,19 +577,19 @@ class NWProject(QObject):
logger.error("Item '%s' has no parent in current tree", tHandle) logger.error("Item '%s' has no parent in current tree", tHandle)
tItem.setParent(None) tItem.setParent(None)
yield tItem yield tItem
return
## ##
# Class Methods # Class Methods
## ##
def updateWordCounts(self): def updateWordCounts(self) -> None:
"""Update the total word count values. """Update the total word count values."""
"""
novel, notes = self._tree.sumWords() novel, notes = self._tree.sumWords()
self._data.setCurrCounts(novel=novel, notes=notes) self._data.setCurrCounts(novel=novel, notes=notes)
return return
def countStatus(self): def countStatus(self) -> None:
"""Count how many times the various status flags are used in the """Count how many times the various status flags are used in the
project tree. The counts themselves are kept in the NWStatus project tree. The counts themselves are kept in the NWStatus
objects. This is essentially a refresh. objects. This is essentially a refresh.
@@ -603,18 +603,18 @@ class NWProject(QObject):
self._data.itemImport.increment(nwItem.itemImport) self._data.itemImport.increment(nwItem.itemImport)
return return
def localLookup(self, theWord): def localLookup(self, word: str | int) -> str:
"""Look up a word in the translation map for the project and """Look up a word or number in the translation map for the
return it. The variable is cast to a string before lookup. If project and return it. The variable is cast to a string before
the word does not exist, it returns itself. lookup. If the word does not exist, it returns itself.
""" """
return self._langData.get(str(theWord), str(theWord)) return self._langData.get(str(word), str(word))
## ##
# Internal Functions # Internal Functions
## ##
def _setStatusImport(self, new, delete, target): def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool:
"""Update the list of novel file status or importance flags, and """Update the list of novel file status or importance flags, and
delete those that have been requested deleted. delete those that have been requested deleted.
""" """
+5 -5
View File
@@ -147,7 +147,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
assert "7a992350f3eb6" in theIndex._itemIndex assert "7a992350f3eb6" in theIndex._itemIndex
# Finalise # Finalise
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreIndex_LoadSave # END Test testCoreIndex_LoadSave
@@ -195,7 +195,7 @@ def testCoreIndex_ScanThis(mockGUI):
assert theBits == ["@tag", "this", "and this"] assert theBits == ["@tag", "this", "and this"]
assert thePos == [0, 6, 12] assert thePos == [0, 6, 12]
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreIndex_ScanThis # END Test testCoreIndex_ScanThis
@@ -273,7 +273,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
assert theIndex.checkThese(["@who", "Jane", "John"], cItem) == [False, False, False] assert theIndex.checkThese(["@who", "Jane", "John"], cItem) == [False, False, False]
assert theIndex.checkThese(["@pov", "Jane", "John"], nItem) == [True, True, False] assert theIndex.checkThese(["@pov", "Jane", "John"], nItem) == [True, True, False]
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreIndex_CheckThese # END Test testCoreIndex_CheckThese
@@ -494,7 +494,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1 assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1
assert theIndex._itemIndex[pHandle]["T0000"].synopsis == "" assert theIndex._itemIndex[pHandle]["T0000"].synopsis == ""
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreIndex_ScanText # END Test testCoreIndex_ScanText
@@ -774,7 +774,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert theIndex.saveIndex() is True assert theIndex.saveIndex() is True
assert theProject.saveProject() is True assert theProject.saveProject() is True
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreIndex_ExtractData # END Test testCoreIndex_ExtractData
+15 -15
View File
@@ -58,7 +58,7 @@ def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd):
assert theProject.projChanged is True assert theProject.projChanged is True
assert theProject.saveProject() is True assert theProject.saveProject() is True
assert theProject.closeProject() is True theProject.closeProject()
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
@@ -154,7 +154,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
assert "0000000000011" not in theProject.tree assert "0000000000011" not in theProject.tree
assert "0000000000012" not in theProject.tree assert "0000000000012" not in theProject.tree
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreProject_NewFileFolder # END Test testCoreProject_NewFileFolder
@@ -182,12 +182,12 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
caplog.clear() caplog.clear()
assert theProject.openProject(fncPath) is True assert theProject.openProject(fncPath) is True
assert "Failed to check lock file" in caplog.text assert "Failed to check lock file" in caplog.text
assert theProject.closeProject() theProject.closeProject()
# Force open with lockfile # Force open with lockfile
assert theProject._storage.writeLockFile() assert theProject._storage.writeLockFile()
assert theProject.openProject(fncPath, overrideLock=True) is True assert theProject.openProject(fncPath, overrideLock=True) is True
assert theProject.closeProject() theProject.closeProject()
assert theProject.getLockStatus() is None assert theProject.getLockStatus() is None
# Fail getting xml reader # Fail getting xml reader
@@ -237,7 +237,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False) mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False)
assert theProject.openProject(fncPath) is True assert theProject.openProject(fncPath) is True
assert theProject.closeProject() theProject.closeProject()
# Trigger an index rebuild # Trigger an index rebuild
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -249,7 +249,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]
assert theProject.index._indexBroken is False assert theProject.index._indexBroken is False
assert theProject.closeProject() theProject.closeProject()
# END Test testCoreProject_Open # END Test testCoreProject_Open
@@ -278,7 +278,7 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
# Save with and without autosave # Save with and without autosave
assert theProject.saveProject(autoSave=False) is True assert theProject.saveProject(autoSave=False) is True
assert theProject.saveProject(autoSave=True) is True assert theProject.saveProject(autoSave=True) is True
assert theProject.closeProject() theProject.closeProject()
# END Test testCoreProject_Save # END Test testCoreProject_Save
@@ -316,7 +316,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
C.hWorldRoot, C.hWorldRoot,
] ]
assert theProject.tree.handles() == oldOrder assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder) theProject.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder assert theProject.tree.handles() == newOrder
# Add a non-existing item # Add a non-existing item
@@ -451,7 +451,7 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
assert len(theProject.data.itemStatus) == 0 assert len(theProject.data.itemStatus) == 0
assert len(theProject.data.itemImport) == 0 assert len(theProject.data.itemImport) == 0
assert theProject.saveProject() is True assert theProject.saveProject() is True
assert theProject.closeProject() is True theProject.closeProject()
# END Test testCoreProject_StatusImport # END Test testCoreProject_StatusImport
@@ -509,9 +509,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
# Project Language # Project Language
theProject.setProjectChanged(False) theProject.setProjectChanged(False)
theProject.data.setLanguage("en") theProject.data.setLanguage("en")
assert theProject.setProjectLang(None) is True theProject.setProjectLang(None)
assert theProject.data.language is None assert theProject.data.language is None
assert theProject.setProjectLang("en_GB") is True theProject.setProjectLang("en_GB")
assert theProject.data.language == "en_GB" assert theProject.data.language == "en_GB"
# Language Lookup # Language Lookup
@@ -562,9 +562,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"000000000000e", "000000000000f", "000000000000e", "000000000000f",
] ]
assert theProject.tree.handles() == oldOrder assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder) theProject.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder assert theProject.tree.handles() == newOrder
assert theProject.setTreeOrder(oldOrder) theProject.setTreeOrder(oldOrder)
assert theProject.tree.handles() == oldOrder assert theProject.tree.handles() == oldOrder
# END Test testCoreProject_Methods # END Test testCoreProject_Methods
@@ -590,7 +590,7 @@ def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
# Save and close # Save and close
assert theProject.saveProject() is True assert theProject.saveProject() is True
assert theProject.closeProject() is True theProject.closeProject()
# First Item with Meta Data # First Item with Meta Data
orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd" orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd"
@@ -645,7 +645,7 @@ def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
assert oItem.itemLayout == nwItemLayout.NOTE assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(prjLipsum) assert theProject.saveProject(prjLipsum)
assert theProject.closeProject() theProject.closeProject()
# Finally, check that the orphaned files function returns # Finally, check that the orphaned files function returns
# if no project is open and no path is set # if no project is open and no path is set
+5 -1
View File
@@ -63,6 +63,7 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert storage.getXmlWriter() is None assert storage.getXmlWriter() is None
assert bool(storage.getDocument(C.hSceneDoc)) is False assert bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None assert storage.getMetaFile("file") is None
assert storage.scanContent() == []
# Open project as a new project should fail # Open project as a new project should fail
assert storage.openProjectInPlace(fncPath, newProject=True) is False assert storage.openProjectInPlace(fncPath, newProject=True) is False
@@ -90,6 +91,9 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert isinstance(storage.getXmlReader(), ProjectXMLReader) assert isinstance(storage.getXmlReader(), ProjectXMLReader)
assert isinstance(storage.getXmlWriter(), ProjectXMLWriter) assert isinstance(storage.getXmlWriter(), ProjectXMLWriter)
# Get content
assert storage.scanContent() == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
# Get document # Get document
assert storage.getDocument(C.hSceneDoc).readDocument() == "### New Scene\n\n" assert storage.getDocument(C.hSceneDoc).readDocument() == "### New Scene\n\n"
@@ -97,7 +101,7 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff" assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff"
# Clean up # Clean up
assert theProject.closeProject() is True theProject.closeProject()
# Check closed project return values (again) # Check closed project return values (again)
assert storage.isOpen() is False assert storage.isOpen() is False