Complete annotations in project class and update tests
This commit is contained in:
@@ -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
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user