Add annotations to tree and item classes

This commit is contained in:
Veronica Berglyd Olsen
2023-07-20 16:00:16 +02:00
parent bba46ba042
commit fe7e44a3a6
4 changed files with 150 additions and 212 deletions
+86 -101
View File
@@ -22,15 +22,23 @@ General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from PyQt5.QtGui import QIcon
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
)
from novelwriter.constants import nwHeaders, nwLabels, trConst
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
@@ -43,7 +51,7 @@ class NWItem:
"_paraCount", "_cursorPos", "_initCount",
)
def __init__(self, project):
def __init__(self, project: NWProject) -> None:
self._project = project
self._name = ""
@@ -69,10 +77,10 @@ class NWItem:
return
def __repr__(self):
def __repr__(self) -> str:
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
def __bool__(self):
def __bool__(self) -> bool:
return self._handle is not None
##
@@ -80,87 +88,86 @@ class NWItem:
##
@property
def itemName(self):
def itemName(self) -> str:
return self._name
@property
def itemHandle(self):
def itemHandle(self) -> str | None:
return self._handle
@property
def itemParent(self):
def itemParent(self) -> str | None:
return self._parent
@property
def itemRoot(self):
def itemRoot(self) -> str | None:
return self._root
@property
def itemOrder(self):
def itemOrder(self) -> int:
return self._order
@property
def itemType(self):
def itemType(self) -> nwItemType:
return self._type
@property
def itemClass(self):
def itemClass(self) -> nwItemClass:
return self._class
@property
def itemLayout(self):
def itemLayout(self) -> nwItemLayout:
return self._layout
@property
def itemStatus(self):
def itemStatus(self) -> str | None:
return self._status
@property
def itemImport(self):
def itemImport(self) -> str | None:
return self._import
@property
def isActive(self):
def isActive(self) -> bool:
return self._active
@property
def isExpanded(self):
def isExpanded(self) -> bool:
return self._expanded
@property
def mainHeading(self):
def mainHeading(self) -> str:
return self._heading
@property
def charCount(self):
def charCount(self) -> int:
return self._charCount
@property
def wordCount(self):
def wordCount(self) -> int:
return self._wordCount
@property
def paraCount(self):
def paraCount(self) -> int:
return self._paraCount
@property
def initCount(self):
def initCount(self) -> int:
return self._initCount
@property
def cursorPos(self):
def cursorPos(self) -> int:
return self._cursorPos
##
# Pack/Unpack Data
##
def pack(self):
"""Pack all the data in the class instance into a dictionary.
"""
item = {}
meta = {}
name = {}
def pack(self) -> dict[str, dict[str, str]]:
"""Pack all the data in the class instance into a dictionary."""
item: dict[str, str] = {}
meta: dict[str, str] = {}
name: dict[str, str] = {}
item["handle"] = str(self._handle)
item["parent"] = str(self._parent)
@@ -190,9 +197,8 @@ class NWItem:
return data
def unpack(self, data):
"""Set the values from a data dictionary.
"""
def unpack(self, data: dict[str, dict[str, Any]]) -> bool:
"""Set the values from a data dictionary."""
item = data.get("itemAttr", {})
meta = data.get("metaAttr", {})
name = data.get("nameAttr", {})
@@ -243,9 +249,8 @@ class NWItem:
# Lookup Methods
##
def describeMe(self):
"""Return a string description of the item.
"""
def describeMe(self) -> str:
"""Return a string description of the item."""
descKey = "none"
if self._type == nwItemType.ROOT:
descKey = "root"
@@ -268,7 +273,7 @@ class NWItem:
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getImportStatus(self, incIcon=True):
def getImportStatus(self, incIcon: bool = True) -> tuple[str, QIcon | None]:
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
@@ -284,51 +289,43 @@ class NWItem:
# Checker Methods
##
def isNovelLike(self):
"""Returns true if the item is of a novel-like class.
"""
def isNovelLike(self) -> bool:
"""Check if the item is of a novel-like class."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
def documentAllowed(self):
"""Returns true if the item is allowed to be of document layout.
"""
def documentAllowed(self) -> bool:
"""Check if the item is allowed to be of document layout."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def isInactiveClass(self):
"""Returns true if the item is in an inactive class.
"""
def isInactiveClass(self) -> bool:
"""Check if the item is in an inactive class."""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def isRootType(self):
def isRootType(self) -> bool:
"""Check if item is a root item."""
return self._type == nwItemType.ROOT
def isFolderType(self):
def isFolderType(self) -> bool:
"""Check if item is a folder item."""
return self._type == nwItemType.FOLDER
def isFileType(self):
def isFileType(self) -> bool:
"""Check if item is a file item."""
return self._type == nwItemType.FILE
def isNoteLayout(self):
def isNoteLayout(self) -> bool:
"""Check if item is a project note."""
return self._layout == nwItemLayout.NOTE
def isDocumentLayout(self):
def isDocumentLayout(self) -> bool:
"""Check if item is a novel document."""
return self._layout == nwItemLayout.DOCUMENT
##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport.
"""
if self.isNovelLike():
self.setStatus(value)
else:
self.setImport(value)
return
def setClassDefaults(self, itemClass):
def setClassDefaults(self, itemClass: nwItemClass) -> None:
"""Set the default values based on the item's class and the
project settings.
"""
@@ -358,27 +355,24 @@ class NWItem:
# Set Item Values
##
def setName(self, name):
"""Set the item name.
"""
def setName(self, name: Any) -> None:
"""Set the item name."""
if isinstance(name, str):
self._name = simplified(name)
else:
self._name = ""
return
def setHandle(self, handle):
"""Set the item handle, and ensure it is valid.
"""
def setHandle(self, handle: Any) -> None:
"""Set the item handle, and ensure it is valid."""
if isHandle(handle):
self._handle = handle
else:
self._handle = None
return
def setParent(self, handle):
"""Set the parent handle, and ensure it is valid.
"""
def setParent(self, handle: Any) -> None:
"""Set the parent handle, and ensure it is valid."""
if handle is None:
self._parent = None
elif isHandle(handle):
@@ -387,9 +381,8 @@ class NWItem:
self._parent = None
return
def setRoot(self, handle):
"""Set the root handle, and ensure it is valid.
"""
def setRoot(self, handle: Any) -> None:
"""Set the root handle, and ensure it is valid."""
if handle is None:
self._root = None
elif isHandle(handle):
@@ -398,7 +391,7 @@ class NWItem:
self._root = None
return
def setOrder(self, order):
def setOrder(self, order: Any) -> None:
"""Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at
the moment.
@@ -406,7 +399,7 @@ class NWItem:
self._order = checkInt(order, 0)
return
def setType(self, value):
def setType(self, value: Any) -> None:
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
@@ -419,7 +412,7 @@ class NWItem:
self._type = nwItemType.NO_TYPE
return
def setClass(self, value):
def setClass(self, value: Any) -> None:
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
@@ -432,7 +425,7 @@ class NWItem:
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, value):
def setLayout(self, value: Any) -> None:
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
@@ -445,32 +438,30 @@ class NWItem:
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, value):
def setStatus(self, value: Any) -> None:
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self._project.data.itemStatus.check(value)
return
def setImport(self, value):
def setImport(self, value: Any) -> None:
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self._project.data.itemImport.check(value)
return
def setActive(self, state):
"""Set the active flag.
"""
def setActive(self, state: Any) -> None:
"""Set the active flag."""
if isinstance(state, bool):
self._active = state
else:
self._active = False
return
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
def setExpanded(self, state: Any) -> None:
"""Set the expanded status of an item in the project tree."""
if isinstance(state, bool):
self._expanded = state
else:
@@ -481,52 +472,46 @@ class NWItem:
# Set Document Meta Data
##
def setMainHeading(self, value):
"""Set the main heading level.
"""
def setMainHeading(self, value: str) -> None:
"""Set the main heading level."""
if value in nwHeaders.H_LEVEL:
self._heading = value
return
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
def setCharCount(self, count: Any) -> None:
"""Set the character count, and ensure that it is an integer."""
if isinstance(count, int):
self._charCount = max(0, count)
else:
self._charCount = 0
return
def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer.
"""
def setWordCount(self, count: Any) -> None:
"""Set the word count, and ensure that it is an integer."""
if isinstance(count, int):
self._wordCount = max(0, count)
else:
self._wordCount = 0
return
def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer.
"""
def setParaCount(self, count: Any) -> None:
"""Set the paragraph count, and ensure that it is an integer."""
if isinstance(count, int):
self._paraCount = max(0, count)
else:
self._paraCount = 0
return
def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer.
"""
def setCursorPos(self, position: Any) -> None:
"""Set the cursor position, and ensure that it is an integer."""
if isinstance(position, int):
self._cursorPos = max(0, position)
else:
self._cursorPos = 0
return
def saveInitialCount(self):
"""Save the initial word count.
"""
def saveInitialCount(self) -> None:
"""Save the initial word count."""
self._initCount = self._wordCount
return
+63 -94
View File
@@ -22,18 +22,23 @@ General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import random
import logging
from typing import TYPE_CHECKING, Any, Iterator
from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.common import checkHandle
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
if TYPE_CHECKING: # pragma: no cover
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
@@ -41,15 +46,16 @@ class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject):
def __init__(self, project: NWProject) -> None:
self.theProject = theProject
self._project = project
self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._projTree: dict[str, NWItem] = {} # Holds all the items of the project
self._treeOrder: list[str] = [] # The order of the tree items on the tree view
self._treeRoots: dict[str, NWItem] = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._treeChanged = False # True if tree structure has changed
return
@@ -58,9 +64,8 @@ class NWTree:
# Class Methods
##
def clear(self):
"""Clear the item tree entirely.
"""
def clear(self) -> None:
"""Clear the item tree entirely."""
self._projTree = {}
self._treeOrder = []
self._treeRoots = {}
@@ -69,14 +74,12 @@ class NWTree:
self._treeChanged = False
return
def handles(self):
"""Returns a copy of the list of all the active handles.
"""
def handles(self) -> list[str]:
"""Returns a copy of the list of all the active handles."""
return self._treeOrder.copy()
def append(self, tHandle, pHandle, nwItem):
"""Add a new item to the end of the tree.
"""
def append(self, tHandle: str | None, pHandle: str | None, nwItem: NWItem) -> bool:
"""Add a new item to the end of the tree."""
tHandle = checkHandle(tHandle, None, True)
pHandle = checkHandle(pHandle, None, True)
if tHandle is None:
@@ -111,9 +114,9 @@ class NWTree:
return True
def pack(self):
"""Pack the content of the tree into the provided XML object. In
the order defined by the _treeOrder list.
def pack(self) -> list[dict[str, dict[str, str]]]:
"""Pack the content of the tree into a list of doctionaries of
items. In the order defined by the _treeOrder list.
"""
tree = []
for tHandle in self._treeOrder:
@@ -122,25 +125,24 @@ class NWTree:
tree.append(tItem.pack())
return tree
def unpack(self, data):
def unpack(self, data: list[dict[str, dict[str, Any]]]) -> None:
"""Iterate through all items of a list and add them to the
project tree.
"""
self.clear()
for item in data:
nwItem = NWItem(self.theProject)
nwItem = NWItem(self._project)
if nwItem.unpack(item):
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount()
return
return True
def writeToCFile(self):
def writeToCFile(self) -> bool:
"""Write the convenience table of contents file in the root of
the project directory.
"""
runtimePath = self.theProject.storage.runtimePath
contentPath = self.theProject.storage.contentPath
runtimePath = self._project.storage.runtimePath
contentPath = self._project.storage.contentPath
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
return False
@@ -184,9 +186,8 @@ class NWTree:
return True
def sumWords(self):
"""Loop over all entries and add up the word counts.
"""
def sumWords(self) -> tuple[int, int]:
"""Loop over all entries and add up the word counts."""
noteWords = 0
novelWords = 0
for tHandle in self._treeOrder:
@@ -205,7 +206,7 @@ class NWTree:
# Tree Item Methods
##
def updateItemData(self, tHandle):
def updateItemData(self, tHandle: str) -> bool:
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
@@ -226,15 +227,14 @@ class NWTree:
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type.
"""
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
def getItemPath(self, tHandle):
def getItemPath(self, tHandle: str) -> list[str]:
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
We do this with a for loop with a maximum depth to make
@@ -263,17 +263,15 @@ class NWTree:
# Tree Root Methods
##
def rootClasses(self):
"""Return a set of all root classes in use by the project.
"""
def rootClasses(self) -> set[nwItemClass]:
"""Return a set of all root classes in use by the project."""
rootClasses = set()
for nwItem in self._treeRoots.values():
rootClasses.add(nwItem.itemClass)
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all root items of a given class in order.
"""
def iterRoots(self, itemClass: nwItemClass | None) -> Iterator[tuple[str, NWItem]]:
"""Iterate over all root items of a given class in order."""
for tHandle in self._treeOrder:
nwItem = self.__getitem__(tHandle)
if isinstance(nwItem, NWItem) and nwItem.isRootType():
@@ -281,14 +279,8 @@ class NWTree:
yield tHandle, nwItem
return
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
return tHandle in self._treeRoots
def isTrash(self, tHandle):
"""Check if an item is in or is the trash folder.
"""
def isTrash(self, tHandle: str) -> bool:
"""Check if an item is in or is the trash folder."""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
@@ -303,7 +295,7 @@ class NWTree:
return True
return False
def trashRoot(self):
def trashRoot(self) -> str | None:
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
@@ -311,14 +303,13 @@ class NWTree:
return self._trashRoot
return None
def findRoot(self, theClass):
"""Find the first root item for a given class.
"""
def findRoot(self, itemClass: nwItemClass) -> str | None:
"""Find the first root item for a given class."""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
if itemClass == tItem.itemClass:
return tItem.itemHandle
return None
@@ -326,9 +317,8 @@ class NWTree:
# Setters
##
def setOrder(self, newOrder):
"""Reorders the tree based on a list of items.
"""
def setOrder(self, newOrder: list[str]) -> None:
"""Reorders the tree based on a list of items."""
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
# Something is wrong, so let's debug it
@@ -346,37 +336,19 @@ class NWTree:
return
def setFileItemLayout(self, tHandle, itemLayout):
"""Set the nwItemLayout for a specific file.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
if not tItem.isFileType():
logger.error("Item '%s' is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
return False
tItem.setLayout(itemLayout)
return True
##
# Special Methods
##
def __len__(self):
"""The number of items in the project.
"""
def __len__(self) -> int:
"""The number of items in the project."""
return len(self._treeOrder)
def __bool__(self):
"""True if there are any items in the project.
"""
def __bool__(self) -> bool:
"""True if there are any items in the project."""
return bool(self._treeOrder)
def __getitem__(self, tHandle):
def __getitem__(self, tHandle: str) -> NWItem | None:
"""Return a project item based on its handle. Returns None if
the handle doesn't exist in the project.
"""
@@ -385,9 +357,8 @@ class NWTree:
logger.error("No tree item with handle '%s'", str(tHandle))
return None
def __delitem__(self, tHandle):
"""Remove an item from the internal lists and dictionaries.
"""
def __delitem__(self, tHandle: str) -> None:
"""Remove an item from the internal lists and dictionaries."""
if tHandle in self._treeOrder and tHandle in self._projTree:
self._treeOrder.remove(tHandle)
del self._projTree[tHandle]
@@ -406,14 +377,12 @@ class NWTree:
return
def __contains__(self, tHandle):
"""Checks if a handle exists in the tree.
"""
def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree."""
return tHandle in self._treeOrder
def __iter__(self):
"""Iterate through project items.
"""
def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items."""
for tHandle in self._treeOrder:
tItem = self._projTree.get(tHandle)
if isinstance(tItem, NWItem):
@@ -424,16 +393,16 @@ class NWTree:
# Internal Functions
##
def _setTreeChanged(self, theState):
def _setTreeChanged(self, state: bool) -> None:
"""Set the changed flag to theState, and if being set to True,
propagate that state change to the parent NWProject class.
"""
self._treeChanged = theState
if theState:
self.theProject.setProjectChanged(True)
self._treeChanged = state
if state:
self._project.setProjectChanged(True)
return
def _makeHandle(self):
def _makeHandle(self) -> str:
"""Generate a unique item handle. In the event that the key
already exists, generate a new one.
"""
-8
View File
@@ -262,19 +262,11 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
assert stT == "Note"
assert isinstance(stI, QIcon)
theItem.setImportStatus(C.sDraft)
stT, stI = theItem.getImportStatus()
assert stT == "Draft"
theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus()
assert stT == "Minor"
assert isinstance(stI, QIcon)
theItem.setImportStatus(C.iMajor)
stT, stI = theItem.getImportStatus()
assert stT == "Major"
# Representation
# ==============
+1 -9
View File
@@ -149,7 +149,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.trashRoot() == "a000000000003"
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
assert theTree.isTrash("a000000000003") is True
assert theTree.isRoot("a000000000002") is True
# Check that we have the root classes
assert theTree.rootClasses() == {
@@ -255,7 +254,7 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
theTree.clear()
assert len(theTree) == 0
assert theTree.handles() == []
assert theTree.unpack(tree) is True
theTree.unpack(tree)
assert theTree.handles() == aHandles
# END Test testCoreTree_PackUnpack
@@ -339,13 +338,6 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001"
]
# Change file layout
assert theTree.setFileItemLayout("stuff", nwItemLayout.DOCUMENT) is False
assert theTree.setFileItemLayout("b000000000001", nwItemLayout.DOCUMENT) is False
assert theTree.setFileItemLayout("c000000000001", "stuff") is False
assert theTree.setFileItemLayout("c000000000001", nwItemLayout.NOTE) is True
assert theTree["c000000000001"].itemLayout == nwItemLayout.NOTE
# END Test testCoreTree_Methods