Rewrite NWItem to not allow handle to be None

This commit is contained in:
Veronica Berglyd Olsen
2023-07-21 22:52:54 +02:00
parent 4dfa51855e
commit 9a1a2d9ab2
7 changed files with 125 additions and 120 deletions
-2
View File
@@ -353,8 +353,6 @@ class BuildSettings:
for item in project.tree: for item in project.tree:
tHandle = item.itemHandle tHandle = item.itemHandle
if tHandle is None:
continue
if item.isInactiveClass() or (item.itemRoot in self._skipRoot): if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
result[tHandle] = (False, FilterMode.SKIPPED) result[tHandle] = (False, FilterMode.SKIPPED)
continue continue
+1 -1
View File
@@ -287,7 +287,7 @@ class DocDuplicator:
hMap: dict[str, str | None] = {t: None for t in items} hMap: dict[str, str | None] = {t: None for t in items}
for tHandle in items: for tHandle in items:
newItem = self._project.tree.duplicate(tHandle) newItem = self._project.tree.duplicate(tHandle)
if newItem is None or newItem.itemHandle is None: if newItem is None:
return return
hMap[tHandle] = newItem.itemHandle hMap[tHandle] = newItem.itemHandle
if newItem.itemParent in hMap: if newItem.itemParent in hMap:
-2
View File
@@ -101,8 +101,6 @@ class NWBuildDocument:
self._queue = [] self._queue = []
filtered = self._build.buildItemFilter(self._project) filtered = self._build.buildItemFilter(self._project)
for item in self._project.tree: for item in self._project.tree:
if not item.itemHandle:
continue
if filtered.get(item.itemHandle, False): if filtered.get(item.itemHandle, False):
self._queue.append(item.itemHandle) self._queue.append(item.itemHandle)
return return
+1 -1
View File
@@ -243,7 +243,7 @@ class NWDocument:
"""Return a pointer to the currently open NWItem.""" """Return a pointer to the currently open NWItem."""
return self._theItem return self._theItem
def getMeta(self) -> tuple[str, str | None, str | None, str | None]: def getMeta(self) -> tuple[str, str | None, nwItemClass | None, nwItemLayout | None]:
"""Parse the document meta tag and return the name, parent, """Parse the document meta tag and return the name, parent,
class and layout meta values. class and layout meta values.
""" """
+43 -40
View File
@@ -1,7 +1,6 @@
""" """
novelWriter Project Item Class novelWriter Project Item Class
================================ ================================
Data class for a project tree item
File History: File History:
Created: 2018-10-27 [0.0.1] Created: 2018-10-27 [0.0.1]
@@ -43,6 +42,13 @@ logger = logging.getLogger(__name__)
class NWItem: class NWItem:
"""Core: Item Data Class
This class holds all the project information about a project item.
Each item must be associated with a project and have a valid handle.
Only the NWTree class should create instances of this class, and
must ensure that the handle is valid for all items in the tree.
"""
__slots__ = ( __slots__ = (
"_project", "_name", "_handle", "_parent", "_root", "_order", "_project", "_name", "_handle", "_parent", "_root", "_order",
@@ -51,11 +57,11 @@ class NWItem:
"_paraCount", "_cursorPos", "_initCount", "_paraCount", "_cursorPos", "_initCount",
) )
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject, handle: str) -> None:
self._project = project self._project = project
self._name = "" self._name = ""
self._handle = None self._handle = handle
self._parent = None self._parent = None
self._root = None self._root = None
self._order = 0 self._order = 0
@@ -81,31 +87,12 @@ class NWItem:
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>" return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""Evaluate to False if itemHandle is not set.""" """The truthiness of the class. The handle used to be initiated
return self._handle is not None to None, but this is no longer the case. It should always
evaluate to True since 2.1-beta1, although unpack and the NWTree
def __copy__(self) -> NWItem: class can leave it as an empty string.
"""Make a shallow copy of the current item.""" """
item = NWItem(self._project) return bool(self._handle)
item._name = self._name
item._handle = self._handle
item._parent = self._parent
item._root = self._root
item._order = self._order
item._type = self._type
item._class = self._class
item._layout = self._layout
item._status = self._status
item._import = self._import
item._active = self._active
item._expanded = self._expanded
item._heading = self._heading
item._charCount = self._charCount
item._wordCount = self._wordCount
item._paraCount = self._paraCount
item._cursorPos = self._cursorPos
item._initCount = self._initCount
return item
## ##
# Properties # Properties
@@ -116,7 +103,7 @@ class NWItem:
return self._name return self._name
@property @property
def itemHandle(self) -> str | None: def itemHandle(self) -> str:
return self._handle return self._handle
@property @property
@@ -184,7 +171,7 @@ class NWItem:
return self._cursorPos return self._cursorPos
## ##
# Pack/Unpack Data # Pack/Unpack/Duplicate Data
## ##
def pack(self) -> dict: def pack(self) -> dict:
@@ -227,8 +214,9 @@ class NWItem:
meta = data.get("metaAttr", {}) meta = data.get("metaAttr", {})
name = data.get("nameAttr", {}) name = data.get("nameAttr", {})
if "handle" in item: handle = item.get("handle", "")
self.setHandle(item["handle"]) if isHandle(handle):
self._handle = handle
else: else:
logger.error("Item does not have a handle") logger.error("Item does not have a handle")
return False return False
@@ -269,6 +257,29 @@ class NWItem:
return True return True
@classmethod
def duplicate(cls, source: NWItem, handle: str) -> NWItem:
"""Make a copy of an item."""
cls = NWItem(source._project, handle)
cls._name = source._name
cls._parent = source._parent
cls._root = source._root
cls._order = source._order
cls._type = source._type
cls._class = source._class
cls._layout = source._layout
cls._status = source._status
cls._import = source._import
cls._active = source._active
cls._expanded = source._expanded
cls._heading = source._heading
cls._charCount = source._charCount
cls._wordCount = source._wordCount
cls._paraCount = source._paraCount
cls._cursorPos = source._cursorPos
cls._initCount = source._initCount
return cls
## ##
# Lookup Methods # Lookup Methods
## ##
@@ -387,14 +398,6 @@ class NWItem:
self._name = "" self._name = ""
return return
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: Any) -> None: def setParent(self, handle: Any) -> None:
"""Set the parent handle, and ensure it is valid.""" """Set the parent handle, and ensure it is valid."""
if handle is None: if handle is None:
+19 -56
View File
@@ -38,7 +38,6 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import trConst, nwLabels from novelwriter.constants import trConst, nwLabels
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex from novelwriter.core.index import NWIndex
from novelwriter.core.options import OptionState from novelwriter.core.options import OptionState
from novelwriter.core.storage import NWStorage from novelwriter.core.storage import NWStorage
@@ -130,42 +129,20 @@ class NWProject(QObject):
# Item Methods # Item Methods
## ##
def newRoot(self, itemClass, label=None): def newRoot(self, itemClass: nwItemClass, label: str | None = None) -> str:
"""Add a new root item. If label is None, use the class label. """Add a new root folder to the project. If label is not set,
use the class label.
""" """
if label is None: label = label or trConst(nwLabels.CLASS_NAME[itemClass])
label = trConst(nwLabels.CLASS_NAME[itemClass]) return self._tree.create(label, None, nwItemType.ROOT, itemClass)
newItem = NWItem(self)
newItem.setName(label)
newItem.setType(nwItemType.ROOT)
newItem.setClass(itemClass)
self._tree.append(None, None, newItem)
self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFolder(self, label, pHandle): def newFolder(self, label: str, parent: str) -> str | None:
"""Add a new folder with a given label and parent item. """Add a new folder with a given label and parent item."""
""" return self._tree.create(label, parent, nwItemType.FOLDER)
if pHandle not in self._tree:
return None
newItem = NWItem(self)
newItem.setName(label)
newItem.setType(nwItemType.FOLDER)
self._tree.append(None, pHandle, newItem)
self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFile(self, label, pHandle): def newFile(self, label: str, parent: str) -> str | None:
"""Add a new file with a given label and parent item. """Add a new file with a given label and parent item."""
""" return self._tree.create(label, parent, nwItemType.FILE)
if pHandle not in self._tree:
return None
newItem = NWItem(self)
newItem.setName(label)
newItem.setType(nwItemType.FILE)
self._tree.append(None, pHandle, newItem)
self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""): def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This """Write content to a new document after it is created. This
@@ -211,18 +188,11 @@ class NWProject(QObject):
return True return True
def trashFolder(self): def trashFolder(self):
"""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:
newItem = NWItem(self) label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])) return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
newItem.setType(nwItemType.ROOT)
newItem.setClass(nwItemClass.TRASH)
self._tree.append(None, None, newItem)
self._tree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
return trashHandle return trashHandle
## ##
@@ -769,11 +739,8 @@ class NWProject(QObject):
oName = self.tr("Recovered File {0}").format(nOrph) oName = self.tr("Recovered File {0}").format(nOrph)
# Recover file meta data # Recover file meta data
if oClass is None: oClass = oClass or nwItemClass.NOVEL
oClass = nwItemClass.NOVEL oLayout = oLayout or nwItemLayout.NOTE
if oLayout is None:
oLayout = nwItemLayout.NOTE
if oParent is None or oParent not in self._tree: if oParent is None or oParent not in self._tree:
oParent = self._tree.findRoot(oClass) oParent = self._tree.findRoot(oClass)
@@ -785,13 +752,9 @@ class NWProject(QObject):
noWhere = True noWhere = True
continue continue
orphItem = NWItem(self) nHandle = self._tree.create(oName, oParent, nwItemType.FILE, oClass, oLayout)
orphItem.setName(oName) if nHandle is not None:
orphItem.setType(nwItemType.FILE) (contentPath / f"{oHandle}.nwd").rename(contentPath / f"{nHandle}.nwd")
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
self._tree.append(oHandle, oParent, orphItem)
self._tree.updateItemData(orphItem.itemHandle)
if noWhere: if noWhere:
self.mainGui.makeAlert(self.tr( self.mainGui.makeAlert(self.tr(
+61 -18
View File
@@ -1,7 +1,6 @@
""" """
novelWriter Project Tree Class novelWriter Project Tree Class
================================ ================================
Data class for the project's tree of project items
File History: File History:
Created: 2020-05-07 [0.4.5] Created: 2020-05-07 [0.4.5]
@@ -24,16 +23,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import copy
import random import random
import logging import logging
from typing import TYPE_CHECKING, Iterator from typing import TYPE_CHECKING, Iterator, overload
from pathlib import Path from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkHandle from novelwriter.common import isHandle
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -44,6 +42,22 @@ logger = logging.getLogger(__name__)
class NWTree: class NWTree:
"""Core: Project Tree Data Class
Only one instance of this class should exist in the project class.
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.
"""
MAX_DEPTH = 1000 # Cap of tree traversing for loops MAX_DEPTH = 1000 # Cap of tree traversing for loops
@@ -52,7 +66,7 @@ class NWTree:
self._project = project self._project = project
self._projTree: dict[str, NWItem] = {} # Holds all the items of the project 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._treeOrder: list[str] = [] # The order of the tree items in the tree view
self._treeRoots: dict[str, NWItem] = {} # The root items of the tree self._treeRoots: dict[str, NWItem] = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder self._trashRoot = None # The handle of the trash root folder
@@ -79,12 +93,44 @@ class NWTree:
"""Returns a copy of the list of all the active handles.""" """Returns a copy of the list of all the active handles."""
return self._treeOrder.copy() return self._treeOrder.copy()
def append(self, tHandle: str | None, pHandle: str | None, nwItem: NWItem) -> bool: @overload
"""Add a new item to the end of the tree.""" def create(self, label: str, parent: None, itemType: nwItemType,
tHandle = checkHandle(tHandle, None, True) itemClass: nwItemClass = nwItemClass.NO_CLASS,
pHandle = checkHandle(pHandle, None, True) itemLayout: nwItemLayout = nwItemLayout.NO_LAYOUT) -> str:
if tHandle is None: ...
@overload
def create(self, label: str, parent: str | None, itemType: nwItemType,
itemClass: nwItemClass = nwItemClass.NO_CLASS,
itemLayout: nwItemLayout = nwItemLayout.NO_LAYOUT) -> str | None:
...
def create(self, label, parent, itemType,
itemClass=nwItemClass.NO_CLASS, itemLayout=nwItemLayout.NO_LAYOUT):
"""Create a new item in the project tree, and return its handle.
If the item cannot be added to the project, None is returned.
"""
if parent is None or parent in self._treeOrder:
tHandle = self._makeHandle() tHandle = self._makeHandle()
newItem = NWItem(self._project, tHandle)
newItem.setName(label)
newItem.setParent(parent)
newItem.setType(itemType)
newItem.setClass(itemClass)
newItem.setLayout(itemLayout)
self.append(newItem)
self.updateItemData(tHandle)
return tHandle
return None
def append(self, nwItem: NWItem) -> bool:
"""Add a new item to the end of the tree."""
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
if not isHandle(tHandle):
logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
return False
if tHandle in self._projTree: if tHandle in self._projTree:
logger.warning("Duplicate handle '%s' detected, skipping", tHandle) logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
@@ -92,9 +138,6 @@ class NWTree:
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle)) logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
if nwItem.isRootType(): if nwItem.isRootType():
logger.debug("Item '%s' is a root item", str(tHandle)) logger.debug("Item '%s' is a root item", str(tHandle))
self._treeRoots[tHandle] = nwItem self._treeRoots[tHandle] = nwItem
@@ -119,8 +162,8 @@ class NWTree:
"""Duplicate an item and set a new handle.""" """Duplicate an item and set a new handle."""
sItem = self.__getitem__(sHandle) sItem = self.__getitem__(sHandle)
if isinstance(sItem, NWItem): if isinstance(sItem, NWItem):
nItem = copy.copy(sItem) nItem = NWItem.duplicate(sItem, self._makeHandle())
if self.append(None, sItem.itemParent, nItem): if self.append(nItem):
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle) logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
return nItem return nItem
return None return None
@@ -142,9 +185,9 @@ class NWTree:
""" """
self.clear() self.clear()
for item in data: for item in data:
nwItem = NWItem(self._project) nwItem = NWItem(self._project, "NOTSET") # Handle is set by unpack()
if nwItem.unpack(item): if nwItem.unpack(item):
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem) self.append(nwItem)
nwItem.saveInitialCount() nwItem.saveInitialCount()
return return
@@ -314,7 +357,7 @@ class NWTree:
return self._trashRoot return self._trashRoot
return None return None
def findRoot(self, itemClass: nwItemClass) -> 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._treeRoots: for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot) tItem = self.__getitem__(aRoot)