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.
"""
def __init__(self, project: NWProject):
def __init__(self, project: NWProject) -> None:
self._project = project
self._state = {}
return
@@ -87,8 +87,7 @@ class OptionState:
##
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)
if not isinstance(stateFile, Path):
return False
@@ -116,7 +115,7 @@ class OptionState:
return True
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)
if not isinstance(stateFile, Path):
return False
+68 -68
View File
@@ -28,6 +28,7 @@ import json
import logging
from time import time
from typing import TYPE_CHECKING, Iterator
from pathlib import Path
from functools import partial
@@ -48,6 +49,11 @@ from novelwriter.common import (
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__)
@@ -55,7 +61,7 @@ class NWProject(QObject):
projectStatusChanged = pyqtSignal(bool)
def __init__(self, mainGui):
def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui)
# Internal
@@ -69,19 +75,14 @@ class NWProject(QObject):
self._index = NWIndex(self) # The project index
self._session = NWSessionLog(self) # The session record
# Data Cache
self._langData = {} # Localisation data
# Project Status
self._langData = {} # Localisation data
self._projChanged = False # The project has unsaved changes
self._lockedBy = None # Data on which computer has the project open
# Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject")
# Set Defaults
self.clearProject()
return
##
@@ -89,23 +90,23 @@ class NWProject(QObject):
##
@property
def options(self):
def options(self) -> OptionState:
return self._options
@property
def storage(self):
def storage(self) -> NWStorage:
return self._storage
@property
def data(self):
def data(self) -> NWProjectData:
return self._data
@property
def tree(self):
def tree(self) -> NWTree:
return self._tree
@property
def index(self):
def index(self) -> NWIndex:
return self._index
@property
@@ -113,11 +114,11 @@ class NWProject(QObject):
return self._session
@property
def projOpened(self):
def projOpened(self) -> float:
return self._session.start
@property
def projChanged(self):
def projChanged(self) -> bool:
return self._projChanged
##
@@ -139,7 +140,7 @@ class NWProject(QObject):
"""Add a new file with a given label and parent item."""
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
will not run if the file exists and is not empty.
"""
@@ -165,7 +166,7 @@ class NWProject(QObject):
return True
def removeItem(self, tHandle):
def removeItem(self, tHandle: str) -> bool:
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
@@ -182,7 +183,7 @@ class NWProject(QObject):
return True
def trashFolder(self):
def trashFolder(self) -> str:
"""Add the special trash root folder to the project."""
trashHandle = self._tree.trashRoot()
if trashHandle is None:
@@ -194,23 +195,28 @@ class NWProject(QObject):
# Project Methods
##
def clearProject(self):
def clearProject(self) -> None:
"""Clear the data for the current project, and set them to
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._data = NWProjectData(self)
self._tree.clear()
self._index.clearIndex()
self._data = NWProjectData(self)
self._session = NWSessionLog(self)
# Project Status
self._langData = {}
self._projChanged = False
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
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
@@ -342,7 +348,7 @@ class NWProject(QObject):
return True
def saveProject(self, autoSave=False):
def saveProject(self, autoSave: bool = False) -> bool:
"""Save the project main XML file. The saving command itself
uses a temporary filename, and the file is replaced afterwards
to make sure if the save fails, we're not left with a truncated
@@ -395,9 +401,8 @@ class NWProject(QObject):
return True
def closeProject(self, idleTime=0.0):
"""Close the current project and clear all meta data.
"""
def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the current project and clear all meta data."""
logger.info("Closing project")
self._options.saveSettings()
self._tree.writeToCFile()
@@ -406,11 +411,10 @@ class NWProject(QObject):
self._storage.closeSession()
self.clearProject()
self._lockedBy = None
return True
return
def backupProject(self, doNotify):
"""Create a zip file of the entire project.
"""
def backupProject(self, doNotify: bool) -> bool:
"""Create a zip file of the entire project."""
if not self._storage.isOpen():
logger.error("No project open")
return False
@@ -467,9 +471,8 @@ class NWProject(QObject):
# Setters
##
def setDefaultStatusImport(self):
"""Set the default status and importance values.
"""
def setDefaultStatusImport(self) -> None:
"""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("Note"), (200, 50, 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))
return
def setProjectLang(self, theLang):
"""Set the project-specific language.
"""
theLang = checkStringNone(theLang, None)
if self._data.language != theLang:
self._data.setLanguage(theLang)
def setProjectLang(self, language: str | None) -> None:
"""Set the project-specific language."""
language = checkStringNone(language, None)
if self._data.language != language:
self._data.setLanguage(language)
self._loadProjectLocalisation()
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
items in the GUI project tree. The user can rearrange the order
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")
self._tree.setOrder(newOrder)
self._tree.setOrder(order)
self.setProjectChanged(True)
return True
return
def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags.
"""
return self._setStatusImport(newCols, delCols, self._data.itemStatus)
def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool:
"""Update the list of novel file status flags."""
return self._setStatusImport(new, deleted, self._data.itemStatus)
def setImportColours(self, newCols, delCols):
"""Update the list of note file importance flags.
"""
return self._setStatusImport(newCols, delCols, self._data.itemImport)
def setImportColours(self, new: list[dict], deleted: list[str]) -> bool:
"""Update the list of note file importance flags."""
return self._setStatusImport(new, deleted, self._data.itemImport)
def setProjectChanged(self, value):
def setProjectChanged(self, status: bool) -> bool:
"""Toggle the project changed flag, and propagate the
information to the GUI statusbar.
"""
if isinstance(value, bool):
self._projChanged = value
if isinstance(status, bool):
self._projChanged = status
self.projectStatusChanged.emit(self._projChanged)
return self._projChanged
@@ -536,7 +536,7 @@ class NWProject(QObject):
"""
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
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,
@@ -577,19 +577,19 @@ class NWProject(QObject):
logger.error("Item '%s' has no parent in current tree", tHandle)
tItem.setParent(None)
yield tItem
return
##
# Class Methods
##
def updateWordCounts(self):
"""Update the total word count values.
"""
def updateWordCounts(self) -> None:
"""Update the total word count values."""
novel, notes = self._tree.sumWords()
self._data.setCurrCounts(novel=novel, notes=notes)
return
def countStatus(self):
def countStatus(self) -> None:
"""Count how many times the various status flags are used in the
project tree. The counts themselves are kept in the NWStatus
objects. This is essentially a refresh.
@@ -603,18 +603,18 @@ class NWProject(QObject):
self._data.itemImport.increment(nwItem.itemImport)
return
def localLookup(self, theWord):
"""Look up a word in the translation map for the project and
return it. The variable is cast to a string before lookup. If
the word does not exist, it returns itself.
def localLookup(self, word: str | int) -> str:
"""Look up a word or number in the translation map for the
project and return it. The variable is cast to a string before
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
##
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
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
# Finalise
assert theProject.closeProject() is True
theProject.closeProject()
# END Test testCoreIndex_LoadSave
@@ -195,7 +195,7 @@ def testCoreIndex_ScanThis(mockGUI):
assert theBits == ["@tag", "this", "and this"]
assert thePos == [0, 6, 12]
assert theProject.closeProject() is True
theProject.closeProject()
# 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(["@pov", "Jane", "John"], nItem) == [True, True, False]
assert theProject.closeProject() is True
theProject.closeProject()
# 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"].synopsis == ""
assert theProject.closeProject() is True
theProject.closeProject()
# END Test testCoreIndex_ScanText
@@ -774,7 +774,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert theIndex.saveIndex() is True
assert theProject.saveProject() is True
assert theProject.closeProject() is True
theProject.closeProject()
# 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.saveProject() is True
assert theProject.closeProject() is True
theProject.closeProject()
copyfile(projFile, testFile)
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 "0000000000012" not in theProject.tree
assert theProject.closeProject() is True
theProject.closeProject()
# END Test testCoreProject_NewFileFolder
@@ -182,12 +182,12 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
caplog.clear()
assert theProject.openProject(fncPath) is True
assert "Failed to check lock file" in caplog.text
assert theProject.closeProject()
theProject.closeProject()
# Force open with lockfile
assert theProject._storage.writeLockFile()
assert theProject.openProject(fncPath, overrideLock=True) is True
assert theProject.closeProject()
theProject.closeProject()
assert theProject.getLockStatus() is None
# 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)
assert theProject.openProject(fncPath) is True
assert theProject.closeProject()
theProject.closeProject()
# Trigger an index rebuild
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 theProject.index._indexBroken is False
assert theProject.closeProject()
theProject.closeProject()
# END Test testCoreProject_Open
@@ -278,7 +278,7 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
# Save with and without autosave
assert theProject.saveProject(autoSave=False) is True
assert theProject.saveProject(autoSave=True) is True
assert theProject.closeProject()
theProject.closeProject()
# END Test testCoreProject_Save
@@ -316,7 +316,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
C.hWorldRoot,
]
assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
theProject.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder
# 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.itemImport) == 0
assert theProject.saveProject() is True
assert theProject.closeProject() is True
theProject.closeProject()
# END Test testCoreProject_StatusImport
@@ -509,9 +509,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
# Project Language
theProject.setProjectChanged(False)
theProject.data.setLanguage("en")
assert theProject.setProjectLang(None) is True
theProject.setProjectLang(None)
assert theProject.data.language is None
assert theProject.setProjectLang("en_GB") is True
theProject.setProjectLang("en_GB")
assert theProject.data.language == "en_GB"
# Language Lookup
@@ -562,9 +562,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"000000000000e", "000000000000f",
]
assert theProject.tree.handles() == oldOrder
assert theProject.setTreeOrder(newOrder)
theProject.setTreeOrder(newOrder)
assert theProject.tree.handles() == newOrder
assert theProject.setTreeOrder(oldOrder)
theProject.setTreeOrder(oldOrder)
assert theProject.tree.handles() == oldOrder
# END Test testCoreProject_Methods
@@ -590,7 +590,7 @@ def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
# Save and close
assert theProject.saveProject() is True
assert theProject.closeProject() is True
theProject.closeProject()
# First Item with Meta Data
orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd"
@@ -645,7 +645,7 @@ def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(prjLipsum)
assert theProject.closeProject()
theProject.closeProject()
# Finally, check that the orphaned files function returns
# 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 bool(storage.getDocument(C.hSceneDoc)) is False
assert storage.getMetaFile("file") is None
assert storage.scanContent() == []
# Open project as a new project should fail
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.getXmlWriter(), ProjectXMLWriter)
# Get content
assert storage.scanContent() == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
# Get document
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"
# Clean up
assert theProject.closeProject() is True
theProject.closeProject()
# Check closed project return values (again)
assert storage.isOpen() is False