Clean up a few things in project classes

This commit is contained in:
Veronica Berglyd Olsen
2023-08-24 19:45:08 +02:00
parent f339ec2471
commit 4507489f0b
8 changed files with 65 additions and 74 deletions
+9 -13
View File
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Iterator
from pathlib import Path from pathlib import Path
from functools import partial from functools import partial
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED, __version__, __hexversion__ from novelwriter import CONFIG, SHARED, __version__, __hexversion__
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
@@ -55,13 +55,9 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWProject(QObject): class NWProject:
statusChanged = pyqtSignal(bool) def __init__(self) -> None:
statusMessage = pyqtSignal(str)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent=parent)
# Core Elements # Core Elements
self._options = OptionState(self) # Project-specific GUI options self._options = OptionState(self) # Project-specific GUI options
@@ -206,7 +202,7 @@ class NWProject(QObject):
def trashFolder(self) -> str: 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:
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]) label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH) return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
@@ -331,7 +327,7 @@ class NWProject(QObject):
self.setProjectChanged(False) self.setProjectChanged(False)
self._valid = True self._valid = True
self.statusMessage.emit(self.tr("Opened Project: {0}").format(self._data.name)) SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
return True return True
@@ -381,7 +377,7 @@ class NWProject(QObject):
) )
self._storage.writeLockFile() self._storage.writeLockFile()
self.statusMessage.emit(self.tr("Saved Project: {0}").format(self._data.name)) SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
self.setProjectChanged(False) self.setProjectChanged(False)
return True return True
@@ -403,7 +399,7 @@ class NWProject(QObject):
return False return False
logger.info("Backing up project") logger.info("Backing up project")
self.statusMessage.emit(self.tr("Backing up project ...")) SHARED.newStatusMessage(self.tr("Backing up project ..."))
if not self._data.name: if not self._data.name:
SHARED.error(self.tr( SHARED.error(self.tr(
@@ -434,7 +430,7 @@ class NWProject(QObject):
SHARED.error(self.tr("Could not write backup archive.")) SHARED.error(self.tr("Could not write backup archive."))
return False return False
self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName))) SHARED.newStatusMessage(self.tr("Project backed up to '{0}'").format(str(archName)))
return True return True
@@ -488,7 +484,7 @@ class NWProject(QObject):
""" """
if isinstance(status, bool): if isinstance(status, bool):
self._changed = status self._changed = status
self.statusChanged.emit(self._changed) SHARED.setGlobalProjectState(self._changed)
return self._changed return self._changed
## ##
+14 -11
View File
@@ -40,6 +40,8 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)
class NWTree: class NWTree:
"""Core: Project Tree Data Class """Core: Project Tree Data Class
@@ -59,7 +61,7 @@ class NWTree:
also used for file names. also used for file names.
""" """
MAX_DEPTH = 1000 # Cap of tree traversing for loops __slots__ = ("_project", "_tree", "_order", "_roots", "_trash", "_changed")
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
@@ -74,6 +76,15 @@ class NWTree:
return return
##
# Properties
##
@property
def trashRoot(self) -> str | None:
"""Return the handle of the trash folder, or None."""
return self._trash
## ##
# Class Methods # Class Methods
## ##
@@ -320,7 +331,7 @@ class NWTree:
return False return False
iItem = tItem iItem = tItem
for _ in range(self.MAX_DEPTH): for _ in range(MAX_DEPTH):
if iItem.itemParent is None: if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle) tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass) tItem.setClassDefaults(iItem.itemClass)
@@ -349,7 +360,7 @@ class NWTree:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for _ in range(self.MAX_DEPTH): for _ in range(MAX_DEPTH):
if tItem.itemParent is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
@@ -400,14 +411,6 @@ class NWTree:
return True return True
return False return False
def trashRoot(self) -> str | None:
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
if self._trash:
return self._trash
return None
def findRoot(self, itemClass: nwItemClass | None) -> str | None: def findRoot(self, itemClass: nwItemClass | None) -> str | None:
"""Find the first root item for a given class.""" """Find the first root item for a given class."""
for aRoot in self._roots: for aRoot in self._roots:
+3 -3
View File
@@ -798,7 +798,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("There is no item to delete") logger.error("There is no item to delete")
return False return False
trashHandle = SHARED.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot
if tHandle == trashHandle: if tHandle == trashHandle:
logger.error("Cannot delete the Trash folder") logger.error("Cannot delete the Trash folder")
return False return False
@@ -823,7 +823,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open") logger.error("No project open")
return False return False
trashHandle = SHARED.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot
logger.debug("Emptying Trash folder") logger.debug("Emptying Trash folder")
if trashHandle is None: if trashHandle is None:
@@ -1201,7 +1201,7 @@ class GuiProjectTree(QTreeWidget):
# Trash Folder # Trash Folder
# ============ # ============
trashHandle = SHARED.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot
if tItem.itemHandle == trashHandle and trashHandle is not None: if tItem.itemHandle == trashHandle and trashHandle is not None:
# The trash folder only has one option # The trash folder only has one option
aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash")) aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash"))
+2 -2
View File
@@ -590,8 +590,8 @@ class GuiMain(QMainWindow):
return True return True
def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool: def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
"""Opens the next document in the project tree, following the """Open the next document in the project tree, following the
document with the given handle. Stops when reaching the end. document with the given handle. Stop when reaching the end.
""" """
if not SHARED.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
+21 -27
View File
@@ -29,7 +29,7 @@ from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QMessageBox, QWidget from PyQt5.QtWidgets import QMessageBox, QWidget
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -84,7 +84,7 @@ class SharedData(QObject):
@property @property
def hasProject(self) -> bool: def hasProject(self) -> bool:
"""Return True of the project instance is populated.""" """Return True if the project instance is populated."""
return self.project.isValid return self.project.isValid
@property @property
@@ -107,9 +107,9 @@ class SharedData(QObject):
## ##
def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None: def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None:
"""Initialise the UserData instance. This must be called as soon """Initialise the SharedData instance. This must be called as
as the Main GUI is created to ensure the SHARED singleton has the soon as the Main GUI is created to ensure the SHARED singleton
properties needed for operation. has the properties needed for operation.
""" """
self._gui = gui self._gui = gui
self._theme = theme self._theme = theme
@@ -159,6 +159,20 @@ class SharedData(QObject):
self._idleRefTime = currTime self._idleRefTime = currTime
return return
def newStatusMessage(self, message: str) -> None:
"""Request a new status message. This is a callable function for
core classes that cannot emit signals on their own.
"""
self.projectStatusMessage.emit(message)
return
def setGlobalProjectState(self, state: bool) -> None:
"""Change the global project status. This is a callable function
for core classes that cannot emit signals on their own.
"""
self.projectStatusChanged.emit(state)
return
## ##
# Alert Boxes # Alert Boxes
## ##
@@ -201,22 +215,6 @@ class SharedData(QObject):
self._alert.exec_() self._alert.exec_()
return self._alert.result() == QMessageBox.Yes return self._alert.result() == QMessageBox.Yes
##
# Internal Slots
##
@pyqtSlot(bool)
def _emitProjectStatusChange(self, state: bool) -> None:
"""Forward the project status slot."""
self.projectStatusChanged.emit(state)
return
@pyqtSlot(str)
def _emitProjectStatusMeesage(self, message: str) -> None:
"""Forward the project message slot."""
self.projectStatusMessage.emit(message)
return
## ##
# Internal Functions # Internal Functions
## ##
@@ -225,12 +223,8 @@ class SharedData(QObject):
"""Create a new project instance.""" """Create a new project instance."""
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
if isinstance(self._project, NWProject): if isinstance(self._project, NWProject):
self._project.statusChanged.disconnect() del self._project
self._project.statusMessage.disconnect() self._project = NWProject()
self._project.deleteLater()
self._project = NWProject(self)
self._project.statusChanged.connect(self._emitProjectStatusChange)
self._project.statusMessage.connect(self._emitProjectStatusMeesage)
return return
def _resetIdleTimer(self) -> None: def _resetIdleTimer(self) -> None:
+12 -14
View File
@@ -119,7 +119,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert bool(theTree) is False assert bool(theTree) is False
# Check for archive and trash folders # Check for archive and trash folders
assert theTree.trashRoot() is None assert theTree.trashRoot is None
aHandles = [] aHandles = []
for nwItem in mockItems: for nwItem in mockItems:
@@ -146,7 +146,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# ============ # ============
# Check that we have the correct archive and trash folders # Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003" assert theTree.trashRoot == "a000000000003"
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002" assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrash("a000000000003") is True assert theTree.isTrash("a000000000003") is True
@@ -261,7 +261,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000003"] del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3 assert len(theTree) == len(mockItems) - 3
assert "a000000000003" not in theTree assert "a000000000003" not in theTree
assert theTree.trashRoot() is None assert theTree.trashRoot is None
# END Test testCoreTree_BuildTree # END Test testCoreTree_BuildTree
@@ -365,7 +365,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc
@pytest.mark.core @pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems): def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
"""Test various class methods.""" """Test various class methods."""
theProject = NWProject() theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
@@ -389,11 +389,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
assert theTree.updateItemData("b000000000001") is True assert theTree.updateItemData("b000000000001") is True
# Update item data, root is unreachable # Update item data, root is unreachable
maxDepth = theTree.MAX_DEPTH with monkeypatch.context() as mp:
theTree.MAX_DEPTH = 0 # type: ignore mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
with pytest.raises(RecursionError): with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001") theTree.updateItemData("b000000000001")
theTree.MAX_DEPTH = maxDepth
# Check type # Check type
assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("blabla", nwItemType.FILE) is False
@@ -424,11 +423,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
] ]
# Cause recursion error # Cause recursion error
maxDepth = theTree.MAX_DEPTH with monkeypatch.context() as mp:
theTree.MAX_DEPTH = 0 # type: ignore mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
with pytest.raises(RecursionError): with pytest.raises(RecursionError):
theTree.getItemPath("c000000000001") theTree.getItemPath("c000000000001")
theTree.MAX_DEPTH = maxDepth
# Break the folder parent handle # Break the folder parent handle
theTree["b000000000001"]._parent = "stuff" # type: ignore theTree["b000000000001"]._parent = "stuff" # type: ignore
+2 -2
View File
@@ -205,7 +205,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree) == 0 assert len(SHARED.project.tree) == 0
assert len(SHARED.project.tree._order) == 0 assert len(SHARED.project.tree._order) == 0
assert len(SHARED.project.tree._roots) == 0 assert len(SHARED.project.tree._roots) == 0
assert SHARED.project.tree.trashRoot() is None assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == "" assert SHARED.project.data.name == ""
assert SHARED.project.data.title == "" assert SHARED.project.data.title == ""
assert SHARED.project.data.author == "" assert SHARED.project.data.author == ""
@@ -225,7 +225,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert len(SHARED.project.tree) == 8 assert len(SHARED.project.tree) == 8
assert len(SHARED.project.tree._order) == 8 assert len(SHARED.project.tree._order) == 8
assert len(SHARED.project.tree._roots) == 4 assert len(SHARED.project.tree._roots) == 4
assert SHARED.project.tree.trashRoot() is None assert SHARED.project.tree.trashRoot is None
assert SHARED.project.data.name == "New Project" assert SHARED.project.data.name == "New Project"
assert SHARED.project.data.title == "New Novel" assert SHARED.project.data.title == "New Novel"
assert SHARED.project.data.author == "Jane Doe" assert SHARED.project.data.author == "Jane Doe"
+2 -2
View File
@@ -348,7 +348,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPat
C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010" "0000000000010"
] ]
trashHandle = SHARED.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot
assert projTree.getTreeFromHandle(trashHandle) == [ assert projTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000012", "0000000000011" trashHandle, "0000000000012", "0000000000011"
] ]
@@ -541,7 +541,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
projTree.setExpandedFromHandle(None, True) projTree.setExpandedFromHandle(None, True)
projTree._addTrashRoot() projTree._addTrashRoot()
hTrashRoot = SHARED.project.tree.trashRoot() hTrashRoot = SHARED.project.tree.trashRoot
projTree.setSelectedHandle(C.hCharRoot) projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE) projTree.newTreeItem(nwItemType.FILE)