From fe7e44a3a6cfc8f86ab20003fc34fe14ed2975fa Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:00:16 +0200 Subject: [PATCH] Add annotations to tree and item classes --- novelwriter/core/item.py | 187 ++++++++++++++---------------- novelwriter/core/tree.py | 157 ++++++++++--------------- tests/test_core/test_core_item.py | 8 -- tests/test_core/test_core_tree.py | 10 +- 4 files changed, 150 insertions(+), 212 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index dfee50a3..7ec29852 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -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 . """ +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"" - 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 diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index da5686db..14501940 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -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 . """ +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. """ diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index b388a548..9fc90dad 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -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 # ============== diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index ee9ce687..746e0539 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -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