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 functools import partial
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED, __version__, __hexversion__
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
@@ -55,13 +55,9 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
class NWProject(QObject):
class NWProject:
statusChanged = pyqtSignal(bool)
statusMessage = pyqtSignal(str)
def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent=parent)
def __init__(self) -> None:
# Core Elements
self._options = OptionState(self) # Project-specific GUI options
@@ -206,7 +202,7 @@ class NWProject(QObject):
def trashFolder(self) -> str:
"""Add the special trash root folder to the project."""
trashHandle = self._tree.trashRoot()
trashHandle = self._tree.trashRoot
if trashHandle is None:
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
@@ -331,7 +327,7 @@ class NWProject(QObject):
self.setProjectChanged(False)
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
@@ -381,7 +377,7 @@ class NWProject(QObject):
)
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)
return True
@@ -403,7 +399,7 @@ class NWProject(QObject):
return False
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:
SHARED.error(self.tr(
@@ -434,7 +430,7 @@ class NWProject(QObject):
SHARED.error(self.tr("Could not write backup archive."))
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
@@ -488,7 +484,7 @@ class NWProject(QObject):
"""
if isinstance(status, bool):
self._changed = status
self.statusChanged.emit(self._changed)
SHARED.setGlobalProjectState(self._changed)
return self._changed
##
+14 -11
View File
@@ -40,6 +40,8 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)
class NWTree:
"""Core: Project Tree Data Class
@@ -59,7 +61,7 @@ class NWTree:
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:
@@ -74,6 +76,15 @@ class NWTree:
return
##
# Properties
##
@property
def trashRoot(self) -> str | None:
"""Return the handle of the trash folder, or None."""
return self._trash
##
# Class Methods
##
@@ -320,7 +331,7 @@ class NWTree:
return False
iItem = tItem
for _ in range(self.MAX_DEPTH):
for _ in range(MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
@@ -349,7 +360,7 @@ class NWTree:
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for _ in range(self.MAX_DEPTH):
for _ in range(MAX_DEPTH):
if tItem.itemParent is None:
return tTree
else:
@@ -400,14 +411,6 @@ class NWTree:
return True
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:
"""Find the first root item for a given class."""
for aRoot in self._roots:
+3 -3
View File
@@ -798,7 +798,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("There is no item to delete")
return False
trashHandle = SHARED.project.tree.trashRoot()
trashHandle = SHARED.project.tree.trashRoot
if tHandle == trashHandle:
logger.error("Cannot delete the Trash folder")
return False
@@ -823,7 +823,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
trashHandle = SHARED.project.tree.trashRoot()
trashHandle = SHARED.project.tree.trashRoot
logger.debug("Emptying Trash folder")
if trashHandle is None:
@@ -1201,7 +1201,7 @@ class GuiProjectTree(QTreeWidget):
# Trash Folder
# ============
trashHandle = SHARED.project.tree.trashRoot()
trashHandle = SHARED.project.tree.trashRoot
if tItem.itemHandle == trashHandle and trashHandle is not None:
# The trash folder only has one option
aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash"))
+2 -2
View File
@@ -590,8 +590,8 @@ class GuiMain(QMainWindow):
return True
def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
"""Opens the next document in the project tree, following the
document with the given handle. Stops when reaching the end.
"""Open the next document in the project tree, following the
document with the given handle. Stop when reaching the end.
"""
if not SHARED.hasProject:
logger.error("No project open")
+21 -27
View File
@@ -29,7 +29,7 @@ from time import time
from typing import TYPE_CHECKING
from pathlib import Path
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QMessageBox, QWidget
if TYPE_CHECKING: # pragma: no cover
@@ -84,7 +84,7 @@ class SharedData(QObject):
@property
def hasProject(self) -> bool:
"""Return True of the project instance is populated."""
"""Return True if the project instance is populated."""
return self.project.isValid
@property
@@ -107,9 +107,9 @@ class SharedData(QObject):
##
def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None:
"""Initialise the UserData instance. This must be called as soon
as the Main GUI is created to ensure the SHARED singleton has the
properties needed for operation.
"""Initialise the SharedData instance. This must be called as
soon as the Main GUI is created to ensure the SHARED singleton
has the properties needed for operation.
"""
self._gui = gui
self._theme = theme
@@ -159,6 +159,20 @@ class SharedData(QObject):
self._idleRefTime = currTime
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
##
@@ -201,22 +215,6 @@ class SharedData(QObject):
self._alert.exec_()
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
##
@@ -225,12 +223,8 @@ class SharedData(QObject):
"""Create a new project instance."""
from novelwriter.core.project import NWProject
if isinstance(self._project, NWProject):
self._project.statusChanged.disconnect()
self._project.statusMessage.disconnect()
self._project.deleteLater()
self._project = NWProject(self)
self._project.statusChanged.connect(self._emitProjectStatusChange)
self._project.statusMessage.connect(self._emitProjectStatusMeesage)
del self._project
self._project = NWProject()
return
def _resetIdleTimer(self) -> None:
+12 -14
View File
@@ -119,7 +119,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert bool(theTree) is False
# Check for archive and trash folders
assert theTree.trashRoot() is None
assert theTree.trashRoot is None
aHandles = []
for nwItem in mockItems:
@@ -146,7 +146,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# ============
# 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.isTrash("a000000000003") is True
@@ -261,7 +261,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3
assert "a000000000003" not in theTree
assert theTree.trashRoot() is None
assert theTree.trashRoot is None
# END Test testCoreTree_BuildTree
@@ -365,7 +365,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc
@pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems):
def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
"""Test various class methods."""
theProject = NWProject()
theTree = NWTree(theProject)
@@ -389,11 +389,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
assert theTree.updateItemData("b000000000001") is True
# Update item data, root is unreachable
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0 # type: ignore
with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001")
theTree.MAX_DEPTH = maxDepth
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001")
# Check type
assert theTree.checkType("blabla", nwItemType.FILE) is False
@@ -424,11 +423,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
]
# Cause recursion error
maxDepth = theTree.MAX_DEPTH
theTree.MAX_DEPTH = 0 # type: ignore
with pytest.raises(RecursionError):
theTree.getItemPath("c000000000001")
theTree.MAX_DEPTH = maxDepth
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
with pytest.raises(RecursionError):
theTree.getItemPath("c000000000001")
# Break the folder parent handle
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._order) == 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.title == ""
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._order) == 8
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.title == "New Novel"
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,
"0000000000010"
]
trashHandle = SHARED.project.tree.trashRoot()
trashHandle = SHARED.project.tree.trashRoot
assert projTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000012", "0000000000011"
]
@@ -541,7 +541,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
projTree.setExpandedFromHandle(None, True)
projTree._addTrashRoot()
hTrashRoot = SHARED.project.tree.trashRoot()
hTrashRoot = SHARED.project.tree.trashRoot
projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE)