Add test coverage of the project tree

This commit is contained in:
Veronica Berglyd Olsen
2024-11-24 02:48:17 +01:00
parent 01b2b53fd5
commit 9ec9c55c3f
5 changed files with 346 additions and 463 deletions
+26
View File
@@ -66,6 +66,25 @@ if TYPE_CHECKING: # pragma: no cover
class ProjectNode:
"""Core: Project Model Node Class
The project tree structure is saved as nodes in a tree, starting
from a root node. This class makes up these nodes.
Each node is a wrapper around an NWItem object. The NWItem is the
object representing a single item in the project, and it only
contains a reference to its parent as well as it top level root, but
is itself not structured in a hierarchy in memory.
This class provides the necessary hierarchical structure, as well as
the data entries needed for populating the GUI project tree. It also
handles pushing and pulling information from its NWItem when
necessary.
The data to be displayed could in principle be pulled from the
NWItem whenever it is needed, but for performance reason it is
cached, as the GUI will pull this information often.
"""
C_NAME = 0
C_COUNT = 1
@@ -254,6 +273,13 @@ class ProjectNode:
class ProjectModel(QAbstractItemModel):
"""Core: Project Model Class
This class provides the interface for the tree widget used on the
GUI. It implements the QModelIndex based interface required, adds
support for drag and drop, and a few other novelWriter-specific
methods needed primarily by the project tree GUI component.
"""
__slots__ = ("_tree", "_root")
+46 -61
View File
@@ -45,7 +45,7 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)
MAX_DEPTH = 999 # Cap of tree traversing for loops (recursion limit)
class NWTree:
@@ -55,12 +55,6 @@ class NWTree:
This class holds all the project items of the project as instances
of NWItem.
For historical reasons, the order of the items is saved in a
separate list from the items themselves, which are stored in a
dictionary. This is somewhat redundant with the newer versions of
Python, but is still practical as it's easier to update the item
order as a list.
Each item has a handle, which is a random hex string of length 13.
The handle is the name of the item everywhere in novelWriter, and is
also used for file names.
@@ -77,10 +71,37 @@ class NWTree:
logger.debug("Ready: NWTree")
return
def __del__(self) -> None:
def __del__(self) -> None: # pragma: no cover
logger.debug("Delete: NWTree")
return
def __len__(self) -> int:
"""The number of items in the project."""
return len(self._items)
def __bool__(self) -> bool:
"""True if there are any items in the project."""
return bool(self._items)
def __getitem__(self, tHandle: str | None) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
if tHandle and tHandle in self._items:
return self._items[tHandle]
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree."""
return tHandle in self._items
def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items."""
for node in self._model.root.allChildren():
yield node.item
return
##
# Properties
##
@@ -148,7 +169,8 @@ class NWTree:
self._itemChange(node.item, nwChange.DELETE)
del self._nodes[tHandle]
del self._items[tHandle]
return True
return True
return False
@overload # pragma: no cover
def create(
@@ -219,7 +241,7 @@ class NWTree:
later = items
self._model.beginInsertRows(self._model.index(0, 0), 0, 0)
for _ in range(999):
for _ in range(MAX_DEPTH):
later = self._addItems(later)
if len(later) == 0:
break
@@ -251,6 +273,8 @@ class NWTree:
for node in reversed(self._model.root.allChildren()):
node.refresh()
node.updateCount(propagate=False)
self._model.root.refresh()
self._model.root.updateCount(propagate=False)
self._model.layoutChanged.emit()
return
@@ -364,35 +388,27 @@ class NWTree:
def checkType(self, tHandle: str, itemType: nwItemType) -> bool:
"""Check if item exists and is of the specified item type."""
tItem = self.__getitem__(tHandle)
if not tItem:
return False
return tItem.itemType == itemType
if tItem := self.__getitem__(tHandle):
return tItem.itemType == itemType
return False
def getItemPath(self, tHandle: str, asName: bool = False) -> list[str]:
def itemPath(self, tHandle: str, asName: bool = False) -> list[str]:
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles, or
alternatively item names. We do this with a for loop with a
maximum depth to make infinite loops impossible.
"""
tTree = []
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tItem.itemName if asName else tHandle)
path = []
if node := self._nodes.get(tHandle):
for _ in range(MAX_DEPTH):
if tItem.itemParent is None:
return tTree
if parent := node.parent():
path.append(node.item.itemName if asName else tHandle)
node = parent
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
else:
tTree.append(tItem.itemName if asName else tHandle)
return path
else:
raise RecursionError("Critical internal error")
return tTree
logger.error("Max project tree depth reached")
return path
def subTree(self, tHandle: str) -> list[str]:
"""Get the subtree from a given handle."""
@@ -426,37 +442,6 @@ class NWTree:
return node.item.itemHandle
return None
##
# Special Methods
##
def __len__(self) -> int:
"""The number of items in the project."""
return len(self._items)
def __bool__(self) -> bool:
"""True if there are any items in the project."""
return bool(self._items)
def __getitem__(self, tHandle: str | None) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
if tHandle and tHandle in self._items:
return self._items[tHandle]
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree."""
return tHandle in self._items
def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items."""
for node in self._model.root.allChildren():
yield node.item
return
##
# Internal Functions
##
+1 -1
View File
@@ -3013,7 +3013,7 @@ class GuiDocEditHeader(QWidget):
if CONFIG.showFullPath:
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
[name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
)))
else:
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")
+1 -1
View File
@@ -806,7 +806,7 @@ class GuiDocViewHeader(QWidget):
if CONFIG.showFullPath:
self.itemTitle.setText(f" {nwUnicode.U_RSAQUO} ".join(reversed(
[name for name in SHARED.project.tree.getItemPath(tHandle, asName=True)]
[name for name in SHARED.project.tree.itemPath(tHandle, asName=True)]
)))
else:
self.itemTitle.setText(i.itemName if (i := SHARED.project.tree[tHandle]) else "")