Improve project item and tree classes (#1482)
This commit is contained in:
@@ -353,8 +353,6 @@ class BuildSettings:
|
||||
|
||||
for item in project.tree:
|
||||
tHandle = item.itemHandle
|
||||
if tHandle is None:
|
||||
continue
|
||||
if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
|
||||
result[tHandle] = (False, FilterMode.SKIPPED)
|
||||
continue
|
||||
|
||||
@@ -287,7 +287,7 @@ class DocDuplicator:
|
||||
hMap: dict[str, str | None] = {t: None for t in items}
|
||||
for tHandle in items:
|
||||
newItem = self._project.tree.duplicate(tHandle)
|
||||
if newItem is None or newItem.itemHandle is None:
|
||||
if newItem is None:
|
||||
return
|
||||
hMap[tHandle] = newItem.itemHandle
|
||||
if newItem.itemParent in hMap:
|
||||
|
||||
@@ -101,8 +101,6 @@ class NWBuildDocument:
|
||||
self._queue = []
|
||||
filtered = self._build.buildItemFilter(self._project)
|
||||
for item in self._project.tree:
|
||||
if not item.itemHandle:
|
||||
continue
|
||||
if filtered.get(item.itemHandle, False):
|
||||
self._queue.append(item.itemHandle)
|
||||
return
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
novelWriter – Project Document
|
||||
==============================
|
||||
Data class for a single novelWriter document
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1]
|
||||
@@ -41,8 +40,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWDocument:
|
||||
"""Core: Document Class
|
||||
|
||||
def __init__(self, project: NWProject, tHandle: str) -> None:
|
||||
A Class wrapping a single novelWriter document file. It represents
|
||||
a project item of nwItemType FILE. The file is not guaranteed to
|
||||
exist, even if the item does. In the case it doesn't exist, reading
|
||||
it returns a None rather than an empty or non-empty string.
|
||||
"""
|
||||
|
||||
def __init__(self, project: NWProject, tHandle: str | None) -> None:
|
||||
|
||||
self._project = project
|
||||
|
||||
@@ -243,7 +249,7 @@ class NWDocument:
|
||||
"""Return a pointer to the currently open NWItem."""
|
||||
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,
|
||||
class and layout meta values.
|
||||
"""
|
||||
|
||||
@@ -76,7 +76,7 @@ class NWIndex:
|
||||
a rebuild of the index data.
|
||||
"""
|
||||
|
||||
def __init__(self, project):
|
||||
def __init__(self, project: NWProject):
|
||||
|
||||
self._project = project
|
||||
|
||||
@@ -197,7 +197,7 @@ class NWIndex:
|
||||
logger.debug("Checking index")
|
||||
|
||||
# Check that all files are indexed
|
||||
for fHandle in self._project.projFiles:
|
||||
for fHandle in self._project.storage.scanContent():
|
||||
if fHandle not in self._itemIndex:
|
||||
logger.warning("Item '%s' is not in the index", fHandle)
|
||||
self.reIndexHandle(fHandle)
|
||||
|
||||
+43
-40
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
novelWriter – Project Item Class
|
||||
================================
|
||||
Data class for a project tree item
|
||||
|
||||
File History:
|
||||
Created: 2018-10-27 [0.0.1]
|
||||
@@ -43,6 +42,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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__ = (
|
||||
"_project", "_name", "_handle", "_parent", "_root", "_order",
|
||||
@@ -51,11 +57,11 @@ class NWItem:
|
||||
"_paraCount", "_cursorPos", "_initCount",
|
||||
)
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
def __init__(self, project: NWProject, handle: str) -> None:
|
||||
|
||||
self._project = project
|
||||
self._name = ""
|
||||
self._handle = None
|
||||
self._handle = handle
|
||||
self._parent = None
|
||||
self._root = None
|
||||
self._order = 0
|
||||
@@ -81,31 +87,12 @@ class NWItem:
|
||||
return f"<NWItem handle={self._handle}, parent={self._parent}, name='{self._name}'>"
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Evaluate to False if itemHandle is not set."""
|
||||
return self._handle is not None
|
||||
|
||||
def __copy__(self) -> NWItem:
|
||||
"""Make a shallow copy of the current item."""
|
||||
item = NWItem(self._project)
|
||||
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
|
||||
"""The truthiness of the class. The handle used to be initiated
|
||||
to None, but this is no longer the case. It should always
|
||||
evaluate to True since 2.1-beta1, although unpack and the NWTree
|
||||
class can leave it as an empty string.
|
||||
"""
|
||||
return bool(self._handle)
|
||||
|
||||
##
|
||||
# Properties
|
||||
@@ -116,7 +103,7 @@ class NWItem:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def itemHandle(self) -> str | None:
|
||||
def itemHandle(self) -> str:
|
||||
return self._handle
|
||||
|
||||
@property
|
||||
@@ -184,7 +171,7 @@ class NWItem:
|
||||
return self._cursorPos
|
||||
|
||||
##
|
||||
# Pack/Unpack Data
|
||||
# Pack/Unpack/Duplicate Data
|
||||
##
|
||||
|
||||
def pack(self) -> dict:
|
||||
@@ -227,8 +214,9 @@ class NWItem:
|
||||
meta = data.get("metaAttr", {})
|
||||
name = data.get("nameAttr", {})
|
||||
|
||||
if "handle" in item:
|
||||
self.setHandle(item["handle"])
|
||||
handle = item.get("handle", "")
|
||||
if isHandle(handle):
|
||||
self._handle = handle
|
||||
else:
|
||||
logger.error("Item does not have a handle")
|
||||
return False
|
||||
@@ -269,6 +257,29 @@ class NWItem:
|
||||
|
||||
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
|
||||
##
|
||||
@@ -387,14 +398,6 @@ class NWItem:
|
||||
self._name = ""
|
||||
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:
|
||||
"""Set the parent handle, and ensure it is valid."""
|
||||
if handle is None:
|
||||
|
||||
@@ -77,7 +77,7 @@ class OptionState:
|
||||
the Config instead.
|
||||
"""
|
||||
|
||||
def __init__(self, project: NWProject):
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
self._project = project
|
||||
self._state = {}
|
||||
return
|
||||
@@ -87,8 +87,7 @@ class OptionState:
|
||||
##
|
||||
|
||||
def loadSettings(self) -> bool:
|
||||
"""Load the options dictionary from the project settings file.
|
||||
"""
|
||||
"""Load the options dictionary from the project."""
|
||||
stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||
if not isinstance(stateFile, Path):
|
||||
return False
|
||||
@@ -116,7 +115,7 @@ class OptionState:
|
||||
return True
|
||||
|
||||
def saveSettings(self) -> bool:
|
||||
"""Save the options dictionary to the project settings file."""
|
||||
"""Save the options dictionary to the project."""
|
||||
stateFile = self._project.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||
if not isinstance(stateFile, Path):
|
||||
return False
|
||||
|
||||
+90
-235
@@ -28,6 +28,7 @@ import json
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
|
||||
@@ -38,7 +39,6 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import trConst, nwLabels
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.index import NWIndex
|
||||
from novelwriter.core.options import OptionState
|
||||
from novelwriter.core.storage import NWStorage
|
||||
@@ -46,9 +46,14 @@ from novelwriter.core.sessions import NWSessionLog
|
||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
|
||||
from novelwriter.core.projectdata import NWProjectData
|
||||
from novelwriter.common import (
|
||||
checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax
|
||||
checkStringNone, formatTimeStamp, hexToInt, makeFileNameSafe, minmax
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from novelwriter.guimain import GuiMain
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.status import NWStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -56,7 +61,7 @@ class NWProject(QObject):
|
||||
|
||||
projectStatusChanged = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
def __init__(self, mainGui: GuiMain) -> None:
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
# Internal
|
||||
@@ -70,20 +75,14 @@ class NWProject(QObject):
|
||||
self._index = NWIndex(self) # The project index
|
||||
self._session = NWSessionLog(self) # The session record
|
||||
|
||||
# Data Cache
|
||||
self._langData = {} # Localisation data
|
||||
|
||||
# Project Status
|
||||
self._langData = {} # Localisation data
|
||||
self._projChanged = False # The project has unsaved changes
|
||||
self._lockedBy = None # Data on which computer has the project open
|
||||
self._projFiles = [] # A list of all files in the content folder on load
|
||||
|
||||
# Internal Mapping
|
||||
self.tr = partial(QCoreApplication.translate, "NWProject")
|
||||
|
||||
# Set Defaults
|
||||
self.clearProject()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -91,23 +90,23 @@ class NWProject(QObject):
|
||||
##
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
def options(self) -> OptionState:
|
||||
return self._options
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
def storage(self) -> NWStorage:
|
||||
return self._storage
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
def data(self) -> NWProjectData:
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def tree(self):
|
||||
def tree(self) -> NWTree:
|
||||
return self._tree
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
def index(self) -> NWIndex:
|
||||
return self._index
|
||||
|
||||
@property
|
||||
@@ -115,59 +114,33 @@ class NWProject(QObject):
|
||||
return self._session
|
||||
|
||||
@property
|
||||
def projOpened(self):
|
||||
def projOpened(self) -> float:
|
||||
return self._session.start
|
||||
|
||||
@property
|
||||
def projChanged(self):
|
||||
def projChanged(self) -> bool:
|
||||
return self._projChanged
|
||||
|
||||
@property
|
||||
def projFiles(self):
|
||||
return self._projFiles
|
||||
|
||||
##
|
||||
# Item Methods
|
||||
##
|
||||
|
||||
def newRoot(self, itemClass, label=None):
|
||||
"""Add a new root item. If label is None, use the class label.
|
||||
def newRoot(self, itemClass: nwItemClass, label: str | None = None) -> str:
|
||||
"""Add a new root folder to the project. If label is not set,
|
||||
use the class label.
|
||||
"""
|
||||
if label is None:
|
||||
label = trConst(nwLabels.CLASS_NAME[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
|
||||
label = label or trConst(nwLabels.CLASS_NAME[itemClass])
|
||||
return self._tree.create(label, None, nwItemType.ROOT, itemClass)
|
||||
|
||||
def newFolder(self, label, pHandle):
|
||||
"""Add a new folder with a given label and parent item.
|
||||
"""
|
||||
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 newFolder(self, label: str, parent: str) -> str | None:
|
||||
"""Add a new folder with a given label and parent item."""
|
||||
return self._tree.create(label, parent, nwItemType.FOLDER)
|
||||
|
||||
def newFile(self, label, pHandle):
|
||||
"""Add a new file with a given label and parent item.
|
||||
"""
|
||||
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 newFile(self, label: str, parent: str) -> str | None:
|
||||
"""Add a new file with a given label and parent item."""
|
||||
return self._tree.create(label, parent, nwItemType.FILE)
|
||||
|
||||
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
|
||||
def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, addText: str = "") -> bool:
|
||||
"""Write content to a new document after it is created. This
|
||||
will not run if the file exists and is not empty.
|
||||
"""
|
||||
@@ -193,7 +166,7 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def removeItem(self, tHandle):
|
||||
def removeItem(self, tHandle: str) -> bool:
|
||||
"""Remove an item from the project. This will delete both the
|
||||
project entry and a document file if it exists.
|
||||
"""
|
||||
@@ -210,45 +183,40 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def trashFolder(self):
|
||||
"""Add the special trash root folder to the project.
|
||||
"""
|
||||
def trashFolder(self) -> str:
|
||||
"""Add the special trash root folder to the project."""
|
||||
trashHandle = self._tree.trashRoot()
|
||||
if trashHandle is None:
|
||||
newItem = NWItem(self)
|
||||
newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]))
|
||||
newItem.setType(nwItemType.ROOT)
|
||||
newItem.setClass(nwItemClass.TRASH)
|
||||
self._tree.append(None, None, newItem)
|
||||
self._tree.updateItemData(newItem.itemHandle)
|
||||
return newItem.itemHandle
|
||||
|
||||
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
||||
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
||||
return trashHandle
|
||||
|
||||
##
|
||||
# Project Methods
|
||||
##
|
||||
|
||||
def clearProject(self):
|
||||
def clearProject(self) -> None:
|
||||
"""Clear the data for the current project, and set them to
|
||||
default values.
|
||||
"""
|
||||
# Project Status
|
||||
self._projChanged = False
|
||||
|
||||
# Project Tree
|
||||
Note: Don't clear the lockedBy data here as it is needed after
|
||||
this function is called.
|
||||
"""
|
||||
# Core Elements
|
||||
self._options = OptionState(self)
|
||||
self._storage.clear()
|
||||
self._data = NWProjectData(self)
|
||||
self._tree.clear()
|
||||
self._index.clearIndex()
|
||||
self._data = NWProjectData(self)
|
||||
self._session = NWSessionLog(self)
|
||||
|
||||
# Project Settings
|
||||
self._projFiles = []
|
||||
# Project Status
|
||||
self._langData = {}
|
||||
self._projChanged = False
|
||||
|
||||
return
|
||||
|
||||
def openProject(self, projPath, overrideLock=False):
|
||||
def openProject(self, projPath: str | Path, overrideLock: bool = False) -> bool:
|
||||
"""Open the project file provided. If it doesn't exist, assume
|
||||
it is a folder and look for the file within it. If successful,
|
||||
parse the XML of the file and populate the project variables and
|
||||
@@ -360,15 +328,13 @@ class NWProject(QObject):
|
||||
)
|
||||
|
||||
# Check the project tree consistency
|
||||
for tItem in self._tree:
|
||||
if tItem:
|
||||
tHandle = tItem.itemHandle
|
||||
logger.debug("Checking item '%s'", tHandle)
|
||||
if not self._tree.updateItemData(tHandle):
|
||||
logger.error("There was a problem the item, and it has been removed")
|
||||
del self._tree[tHandle] # The file will be re-added as orphaned
|
||||
# This also handles any orphaned files found
|
||||
orphans, recovered = self._tree.checkConsistency(self.tr("Recovered"))
|
||||
if orphans > 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Found {0} orphaned file(s) in the project. {1} file(s) were recovered."
|
||||
).format(orphans, recovered), nwAlert.WARN)
|
||||
|
||||
self._scanProjectFolder()
|
||||
self._index.loadIndex()
|
||||
if xmlReader.state == XMLReadState.WAS_LEGACY:
|
||||
# Often, the index needs to be rebuilt when updating format
|
||||
@@ -382,7 +348,7 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def saveProject(self, autoSave=False):
|
||||
def saveProject(self, autoSave: bool = False) -> bool:
|
||||
"""Save the project main XML file. The saving command itself
|
||||
uses a temporary filename, and the file is replaced afterwards
|
||||
to make sure if the save fails, we're not left with a truncated
|
||||
@@ -435,22 +401,19 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def closeProject(self, idleTime=0.0):
|
||||
"""Close the current project and clear all meta data.
|
||||
"""
|
||||
def closeProject(self, idleTime: float = 0.0) -> None:
|
||||
"""Close the current project and clear all meta data."""
|
||||
logger.info("Closing project")
|
||||
self._options.saveSettings()
|
||||
self._tree.writeToCFile()
|
||||
self._session.appendSession(idleTime)
|
||||
self._storage.clearLockFile()
|
||||
self._storage.closeSession()
|
||||
self.clearProject()
|
||||
self._lockedBy = None
|
||||
return True
|
||||
return
|
||||
|
||||
def backupProject(self, doNotify):
|
||||
"""Create a zip file of the entire project.
|
||||
"""
|
||||
def backupProject(self, doNotify: bool) -> bool:
|
||||
"""Create a zip file of the entire project."""
|
||||
if not self._storage.isOpen():
|
||||
logger.error("No project open")
|
||||
return False
|
||||
@@ -507,9 +470,8 @@ class NWProject(QObject):
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setDefaultStatusImport(self):
|
||||
"""Set the default status and importance values.
|
||||
"""
|
||||
def setDefaultStatusImport(self) -> None:
|
||||
"""Set the default status and importance values."""
|
||||
self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100))
|
||||
self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0))
|
||||
self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0))
|
||||
@@ -520,43 +482,40 @@ class NWProject(QObject):
|
||||
self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0))
|
||||
return
|
||||
|
||||
def setProjectLang(self, theLang):
|
||||
"""Set the project-specific language.
|
||||
"""
|
||||
theLang = checkStringNone(theLang, None)
|
||||
if self._data.language != theLang:
|
||||
self._data.setLanguage(theLang)
|
||||
def setProjectLang(self, language: str | None) -> None:
|
||||
"""Set the project-specific language."""
|
||||
language = checkStringNone(language, None)
|
||||
if self._data.language != language:
|
||||
self._data.setLanguage(language)
|
||||
self._loadProjectLocalisation()
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
return
|
||||
|
||||
def setTreeOrder(self, newOrder):
|
||||
def setTreeOrder(self, order: list[str]) -> None:
|
||||
"""A list representing the linear/flattened order of project
|
||||
items in the GUI project tree. The user can rearrange the order
|
||||
by drag-and-drop. Forwarded to the NWTree class.
|
||||
"""
|
||||
if len(self._tree) != len(newOrder):
|
||||
if len(self._tree) != len(order):
|
||||
logger.warning("Sizes of new and old tree order do not match")
|
||||
self._tree.setOrder(newOrder)
|
||||
self._tree.setOrder(order)
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
return
|
||||
|
||||
def setStatusColours(self, newCols, delCols):
|
||||
"""Update the list of novel file status flags.
|
||||
"""
|
||||
return self._setStatusImport(newCols, delCols, self._data.itemStatus)
|
||||
def setStatusColours(self, new: list[dict], deleted: list[str]) -> bool:
|
||||
"""Update the list of novel file status flags."""
|
||||
return self._setStatusImport(new, deleted, self._data.itemStatus)
|
||||
|
||||
def setImportColours(self, newCols, delCols):
|
||||
"""Update the list of note file importance flags.
|
||||
"""
|
||||
return self._setStatusImport(newCols, delCols, self._data.itemImport)
|
||||
def setImportColours(self, new: list[dict], deleted: list[str]) -> bool:
|
||||
"""Update the list of note file importance flags."""
|
||||
return self._setStatusImport(new, deleted, self._data.itemImport)
|
||||
|
||||
def setProjectChanged(self, value):
|
||||
def setProjectChanged(self, status: bool) -> bool:
|
||||
"""Toggle the project changed flag, and propagate the
|
||||
information to the GUI statusbar.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
self._projChanged = value
|
||||
if isinstance(status, bool):
|
||||
self._projChanged = status
|
||||
self.projectStatusChanged.emit(self._projChanged)
|
||||
return self._projChanged
|
||||
|
||||
@@ -576,7 +535,7 @@ class NWProject(QObject):
|
||||
"""
|
||||
return self._data.editTime + round(time() - self._session.start)
|
||||
|
||||
def getProjectItems(self):
|
||||
def getProjectItems(self) -> Iterator[NWItem]:
|
||||
"""This function ensures that the item tree loaded is sent to
|
||||
the GUI tree view in such a way that the tree can be built. That
|
||||
is, the parent item must be sent before its child. In principle,
|
||||
@@ -617,19 +576,19 @@ class NWProject(QObject):
|
||||
logger.error("Item '%s' has no parent in current tree", tHandle)
|
||||
tItem.setParent(None)
|
||||
yield tItem
|
||||
return
|
||||
|
||||
##
|
||||
# Class Methods
|
||||
##
|
||||
|
||||
def updateWordCounts(self):
|
||||
"""Update the total word count values.
|
||||
"""
|
||||
def updateWordCounts(self) -> None:
|
||||
"""Update the total word count values."""
|
||||
novel, notes = self._tree.sumWords()
|
||||
self._data.setCurrCounts(novel=novel, notes=notes)
|
||||
return
|
||||
|
||||
def countStatus(self):
|
||||
def countStatus(self) -> None:
|
||||
"""Count how many times the various status flags are used in the
|
||||
project tree. The counts themselves are kept in the NWStatus
|
||||
objects. This is essentially a refresh.
|
||||
@@ -643,18 +602,18 @@ class NWProject(QObject):
|
||||
self._data.itemImport.increment(nwItem.itemImport)
|
||||
return
|
||||
|
||||
def localLookup(self, theWord):
|
||||
"""Look up a word in the translation map for the project and
|
||||
return it. The variable is cast to a string before lookup. If
|
||||
the word does not exist, it returns itself.
|
||||
def localLookup(self, word: str | int) -> str:
|
||||
"""Look up a word or number in the translation map for the
|
||||
project and return it. The variable is cast to a string before
|
||||
lookup. If the word does not exist, it returns itself.
|
||||
"""
|
||||
return self._langData.get(str(theWord), str(theWord))
|
||||
return self._langData.get(str(word), str(word))
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _setStatusImport(self, new, delete, target):
|
||||
def _setStatusImport(self, new: list[dict], delete: list[str], target: NWStatus) -> bool:
|
||||
"""Update the list of novel file status or importance flags, and
|
||||
delete those that have been requested deleted.
|
||||
"""
|
||||
@@ -676,9 +635,8 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def _loadProjectLocalisation(self):
|
||||
"""Load the language data for the current project language.
|
||||
"""
|
||||
def _loadProjectLocalisation(self) -> bool:
|
||||
"""Load the language data for the current project language."""
|
||||
if self._data.language is None or CONFIG._nwLangPath is None:
|
||||
self._langData = {}
|
||||
return False
|
||||
@@ -691,7 +649,6 @@ class NWProject(QObject):
|
||||
with open(langFile, mode="r", encoding="utf-8") as inFile:
|
||||
self._langData = json.load(inFile)
|
||||
logger.debug("Loaded project language file: %s", langFile.name)
|
||||
|
||||
except Exception:
|
||||
logger.error("Failed to project language file")
|
||||
logException()
|
||||
@@ -699,106 +656,4 @@ class NWProject(QObject):
|
||||
|
||||
return True
|
||||
|
||||
def _scanProjectFolder(self):
|
||||
"""Scan the project folder and check that the files in it are
|
||||
also in the project XML file. If they aren't, import them as
|
||||
orphaned files so the user can either delete them, or put them
|
||||
back into the project tree.
|
||||
"""
|
||||
contentPath = self._storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
return False
|
||||
|
||||
# Then check the files in the data folder
|
||||
logger.debug("Checking files in project content folder")
|
||||
orphanFiles = []
|
||||
self._projFiles = []
|
||||
|
||||
for item in contentPath.iterdir():
|
||||
itemName = item.name
|
||||
if not itemName.endswith(".nwd"):
|
||||
logger.warning("Skipping file: %s", itemName)
|
||||
continue
|
||||
if len(itemName) != 17:
|
||||
logger.warning("Skipping file: %s", itemName)
|
||||
continue
|
||||
|
||||
fHandle = itemName[:13]
|
||||
if not isHandle(fHandle):
|
||||
logger.warning("Skipping file: %s", itemName)
|
||||
continue
|
||||
|
||||
if fHandle in self._tree:
|
||||
self._projFiles.append(fHandle)
|
||||
logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle)
|
||||
else:
|
||||
logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
|
||||
orphanFiles.append(fHandle)
|
||||
|
||||
# Report status
|
||||
if len(orphanFiles) > 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Found {0} orphaned file(s) in project folder."
|
||||
).format(len(orphanFiles)), nwAlert.WARN)
|
||||
else:
|
||||
logger.debug("File check OK")
|
||||
return
|
||||
|
||||
# Handle orphans
|
||||
nOrph = 0
|
||||
noWhere = False
|
||||
oPrefix = self.tr("Recovered")
|
||||
for oHandle in orphanFiles:
|
||||
|
||||
# Look for meta data
|
||||
oName = ""
|
||||
oParent = None
|
||||
oClass = None
|
||||
oLayout = None
|
||||
|
||||
aDoc = self._storage.getDocument(oHandle)
|
||||
if aDoc.readDocument(isOrphan=True) is not None:
|
||||
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
||||
|
||||
if oName:
|
||||
oName = self.tr("[{0}] {1}").format(
|
||||
oPrefix, oName.replace("[%s]" % oPrefix, "").strip()
|
||||
)
|
||||
else:
|
||||
nOrph += 1
|
||||
oName = self.tr("Recovered File {0}").format(nOrph)
|
||||
|
||||
# Recover file meta data
|
||||
if oClass is None:
|
||||
oClass = nwItemClass.NOVEL
|
||||
|
||||
if oLayout is None:
|
||||
oLayout = nwItemLayout.NOTE
|
||||
|
||||
if oParent is None or oParent not in self._tree:
|
||||
oParent = self._tree.findRoot(oClass)
|
||||
if oParent is None:
|
||||
oParent = self._tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
# If the file still has no parent item, skip it
|
||||
if oParent is None:
|
||||
noWhere = True
|
||||
continue
|
||||
|
||||
orphItem = NWItem(self)
|
||||
orphItem.setName(oName)
|
||||
orphItem.setType(nwItemType.FILE)
|
||||
orphItem.setClass(oClass)
|
||||
orphItem.setLayout(oLayout)
|
||||
self._tree.append(oHandle, oParent, orphItem)
|
||||
self._tree.updateItemData(orphItem.itemHandle)
|
||||
|
||||
if noWhere:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"One or more orphaned files could not be added back into the project. "
|
||||
"Make sure at least a Novel root folder exists."
|
||||
), nwAlert.WARN)
|
||||
|
||||
return True
|
||||
|
||||
# END Class NWProject
|
||||
|
||||
@@ -33,7 +33,7 @@ from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import minmax
|
||||
from novelwriter.common import isHandle, minmax
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.document import NWDocument
|
||||
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
|
||||
@@ -67,6 +67,7 @@ class NWStorage:
|
||||
"""Reset internal variables."""
|
||||
self._storagePath = None
|
||||
self._runtimePath = None
|
||||
self._lockFilePath = None
|
||||
self._openMode = self.MODE_INACTIVE
|
||||
return
|
||||
|
||||
@@ -146,7 +147,7 @@ class NWStorage:
|
||||
|
||||
def closeSession(self):
|
||||
"""Run tasks related to closing the session."""
|
||||
# Clear lockfile
|
||||
self.clearLockFile()
|
||||
self.clear()
|
||||
return
|
||||
|
||||
@@ -179,7 +180,17 @@ class NWStorage:
|
||||
return self._runtimePath / "meta" / fileName
|
||||
return None
|
||||
|
||||
def readLockFile(self) -> list:
|
||||
def scanContent(self) -> list[str]:
|
||||
"""Scan the content folder and return the handle of all files
|
||||
found in it. Files that do not match the pattern are ignored.
|
||||
"""
|
||||
contentPath = self.contentPath
|
||||
return [
|
||||
item.stem for item in contentPath.iterdir()
|
||||
if item.suffix == ".nwd" and isHandle(item.stem)
|
||||
] if contentPath else []
|
||||
|
||||
def readLockFile(self) -> list[str]:
|
||||
"""Read the project lock file."""
|
||||
if self._lockFilePath is None:
|
||||
return ["ERROR"]
|
||||
@@ -188,7 +199,7 @@ class NWStorage:
|
||||
return []
|
||||
|
||||
try:
|
||||
lines = self._lockFilePath.read_text(encoding="utf-8").split(";")
|
||||
lines = self._lockFilePath.read_text(encoding="utf-8").strip().split(";")
|
||||
except Exception:
|
||||
logger.error("Failed to read project lockfile")
|
||||
logException()
|
||||
|
||||
+115
-18
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
novelWriter – Project Tree Class
|
||||
================================
|
||||
Data class for the project's tree of project items
|
||||
|
||||
File History:
|
||||
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
|
||||
|
||||
import copy
|
||||
import random
|
||||
import logging
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from typing import TYPE_CHECKING, Iterator, Literal, overload
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkHandle
|
||||
from novelwriter.common import isHandle
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.item import NWItem
|
||||
|
||||
@@ -44,6 +42,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -52,7 +66,7 @@ class NWTree:
|
||||
self._project = 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._trashRoot = None # The handle of the trash root folder
|
||||
@@ -79,12 +93,42 @@ class NWTree:
|
||||
"""Returns a copy of the list of all the active handles."""
|
||||
return self._treeOrder.copy()
|
||||
|
||||
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:
|
||||
@overload
|
||||
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT],
|
||||
itemClass: nwItemClass) -> str: # pragma: no cover
|
||||
pass
|
||||
|
||||
@overload
|
||||
def create(self, label: str, parent: str | None, itemType: nwItemType,
|
||||
itemClass: nwItemClass = nwItemClass.NO_CLASS) -> str | None: # pragma: no cover
|
||||
pass
|
||||
|
||||
def create(self, label, parent, itemType, itemClass=nwItemClass.NO_CLASS):
|
||||
"""Create a new item in the project tree, and return its handle.
|
||||
If the item cannot be added to the project because of an invalid
|
||||
parent, None is returned. For root elements, this cannot occur.
|
||||
"""
|
||||
parent = None if itemType == nwItemType.ROOT else parent
|
||||
if parent is None or parent in self._treeOrder:
|
||||
tHandle = self._makeHandle()
|
||||
newItem = NWItem(self._project, tHandle)
|
||||
newItem.setName(label)
|
||||
newItem.setParent(parent)
|
||||
newItem.setType(itemType)
|
||||
newItem.setClass(itemClass)
|
||||
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:
|
||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||
@@ -92,9 +136,6 @@ class NWTree:
|
||||
|
||||
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
|
||||
nwItem.setHandle(tHandle)
|
||||
nwItem.setParent(pHandle)
|
||||
|
||||
if nwItem.isRootType():
|
||||
logger.debug("Item '%s' is a root item", str(tHandle))
|
||||
self._treeRoots[tHandle] = nwItem
|
||||
@@ -119,8 +160,8 @@ class NWTree:
|
||||
"""Duplicate an item and set a new handle."""
|
||||
sItem = self.__getitem__(sHandle)
|
||||
if isinstance(sItem, NWItem):
|
||||
nItem = copy.copy(sItem)
|
||||
if self.append(None, sItem.itemParent, nItem):
|
||||
nItem = NWItem.duplicate(sItem, self._makeHandle())
|
||||
if self.append(nItem):
|
||||
logger.info("Duplicated item '%s' -> '%s'", sHandle, nItem.itemHandle)
|
||||
return nItem
|
||||
return None
|
||||
@@ -142,12 +183,68 @@ class NWTree:
|
||||
"""
|
||||
self.clear()
|
||||
for item in data:
|
||||
nwItem = NWItem(self._project)
|
||||
nwItem = NWItem(self._project, "") # Handle is set by unpack()
|
||||
if nwItem.unpack(item):
|
||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||
self.append(nwItem)
|
||||
nwItem.saveInitialCount()
|
||||
return
|
||||
|
||||
def checkConsistency(self, prefix: str) -> tuple[int, int]:
|
||||
"""Check the project tree consistency. Also check the content
|
||||
folder and add back files that were discovered but were not
|
||||
included in the tree. This function should only be called after
|
||||
the project file has been processed, but before the loading of
|
||||
the project returns. The functions requires a prefix string to
|
||||
mark recovered files.
|
||||
"""
|
||||
storage = self._project.storage
|
||||
files = set(storage.scanContent())
|
||||
for tHandle in self._treeOrder:
|
||||
if self.updateItemData(tHandle):
|
||||
logger.debug("Checking item '%s' ... OK", tHandle)
|
||||
files.discard(tHandle) # Remove it from the record
|
||||
else:
|
||||
logger.error("Checking item '%s' ... ERROR", tHandle)
|
||||
self.__delitem__(tHandle) # The file will be re-added as orphaned
|
||||
|
||||
orphans = len(files)
|
||||
if orphans == 0:
|
||||
logger.info("Checked project files: OK")
|
||||
return 0, 0
|
||||
|
||||
logger.warning("Found %d file(s) not tracked in project", orphans)
|
||||
recovered = 0
|
||||
for cHandle in files:
|
||||
aDoc = storage.getDocument(cHandle)
|
||||
aDoc.readDocument(isOrphan=True)
|
||||
oName, oParent, oClass, oLayout = aDoc.getMeta()
|
||||
|
||||
oName = oName or cHandle
|
||||
oParent = oParent if oParent in self._treeOrder else None
|
||||
oClass = oClass or nwItemClass.NOVEL
|
||||
oLayout = oLayout or nwItemLayout.NOTE
|
||||
|
||||
# If the parent doesn't exists, find a new home
|
||||
if oParent is None: # Add it to the first available class root
|
||||
oParent = self.findRoot(oClass)
|
||||
if oParent is None: # Otherwise, add to the Novel root
|
||||
oParent = self.findRoot(nwItemClass.NOVEL)
|
||||
if oParent is None: # If not, give up
|
||||
continue
|
||||
|
||||
# Create a new item
|
||||
newItem = NWItem(self._project, cHandle)
|
||||
newItem.setName(f"[{prefix}] {oName}")
|
||||
newItem.setParent(oParent)
|
||||
newItem.setType(nwItemType.FILE)
|
||||
newItem.setClass(oClass)
|
||||
newItem.setLayout(oLayout)
|
||||
if self.append(newItem):
|
||||
self.updateItemData(cHandle)
|
||||
recovered += 1
|
||||
|
||||
return orphans, recovered
|
||||
|
||||
def writeToCFile(self) -> bool:
|
||||
"""Write the convenience table of contents file in the root of
|
||||
the project directory.
|
||||
@@ -314,7 +411,7 @@ class NWTree:
|
||||
return self._trashRoot
|
||||
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."""
|
||||
for aRoot in self._treeRoots:
|
||||
tItem = self.__getitem__(aRoot)
|
||||
|
||||
@@ -1046,13 +1046,12 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
logger.debug("Building the project tree ...")
|
||||
self.clearTree()
|
||||
|
||||
iCount = 0
|
||||
count = 0
|
||||
for nwItem in self.theProject.getProjectItems():
|
||||
iCount += 1
|
||||
count += 1
|
||||
self._addTreeItem(nwItem)
|
||||
|
||||
logger.debug("%d item(s) added to the project tree", iCount)
|
||||
if count > 0:
|
||||
logger.info("%d item(s) added to the project tree", count)
|
||||
return
|
||||
|
||||
def undoLastMove(self):
|
||||
|
||||
@@ -31,7 +31,6 @@ from mocked import causeOSError
|
||||
|
||||
from novelwriter.enum import nwBuildFmt, nwItemClass
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.buildsettings import BuildCollection, BuildSettings, FilterMode
|
||||
|
||||
@@ -229,7 +228,6 @@ def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
|
||||
hArchRoot = project.newRoot(nwItemClass.ARCHIVE, "Archive")
|
||||
hPlotDoc = project.newFile("Main Plot", C.hPlotRoot)
|
||||
hCharDoc = project.newFile("Jane Doe", C.hCharRoot)
|
||||
initLen = len(project.tree)
|
||||
|
||||
# With no changes
|
||||
assert build.isRootAllowed(C.hNovelRoot) is True
|
||||
@@ -361,13 +359,6 @@ def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
|
||||
hCharDoc: (False, FilterMode.FILTERED),
|
||||
}
|
||||
|
||||
# Check error handling
|
||||
project.tree._treeOrder.append("00000000000ff")
|
||||
project.tree._projTree["00000000000ff"] = NWItem(project)
|
||||
assert project.tree["00000000000ff"].itemHandle is None # type: ignore
|
||||
filtered = build.buildItemFilter(project, withRoots=False)
|
||||
assert len(filtered) == initLen
|
||||
|
||||
# No valid project provided
|
||||
assert build.buildItemFilter(None) == {} # type: ignore
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from tools import C, ODT_IGNORE, buildTestProject, cmpFiles
|
||||
from mocked import causeException, causeOSError
|
||||
|
||||
from novelwriter.enum import nwBuildFmt
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.tomd import ToMarkdown
|
||||
from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.tohtml import ToHtml
|
||||
@@ -421,19 +420,15 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
|
||||
docFile.unlink()
|
||||
|
||||
# Add an invalid item to the project
|
||||
bHandle = "0123456789abc"
|
||||
nHandle = "0123456789def"
|
||||
project.tree._treeOrder.append(bHandle)
|
||||
project.tree._projTree[bHandle] = NWItem(project) # Handle should be None
|
||||
project.tree._treeOrder.append(nHandle)
|
||||
project.tree._projTree[nHandle] = None
|
||||
project.tree._projTree[nHandle] = None # type: ignore
|
||||
|
||||
docBuild.queueAll()
|
||||
assert len(docBuild) == 8
|
||||
|
||||
docBuild.addDocument(bHandle)
|
||||
docBuild.addDocument(nHandle)
|
||||
assert len(docBuild) == 10
|
||||
assert len(docBuild) == 9
|
||||
|
||||
# Build the doc again with broken items
|
||||
count = 0
|
||||
@@ -472,7 +467,7 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
|
||||
project.storage.getDocument(hCharDoc).writeDocument("# Jane Doe\n~~Text~~")
|
||||
|
||||
# Fix project order as this has never been opened in a GUI
|
||||
project.tree.setOrder([
|
||||
project.tree.setOrder([ # type: ignore
|
||||
C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
|
||||
C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, C.hWorldRoot
|
||||
])
|
||||
|
||||
@@ -147,7 +147,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
|
||||
assert "7a992350f3eb6" in theIndex._itemIndex
|
||||
|
||||
# Finalise
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_LoadSave
|
||||
|
||||
@@ -195,7 +195,7 @@ def testCoreIndex_ScanThis(mockGUI):
|
||||
assert theBits == ["@tag", "this", "and this"]
|
||||
assert thePos == [0, 6, 12]
|
||||
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_ScanThis
|
||||
|
||||
@@ -273,7 +273,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
|
||||
assert theIndex.checkThese(["@who", "Jane", "John"], cItem) == [False, False, False]
|
||||
assert theIndex.checkThese(["@pov", "Jane", "John"], nItem) == [True, True, False]
|
||||
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_CheckThese
|
||||
|
||||
@@ -494,7 +494,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
|
||||
assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1
|
||||
assert theIndex._itemIndex[pHandle]["T0000"].synopsis == ""
|
||||
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_ScanText
|
||||
|
||||
@@ -774,7 +774,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
|
||||
|
||||
assert theIndex.saveIndex() is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreIndex_ExtractData
|
||||
|
||||
|
||||
@@ -37,7 +37,8 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
|
||||
theProject = NWProject(mockGUI)
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
assert theItem.itemHandle == "0000000000000"
|
||||
|
||||
statusKeys = ["s000000", "s000001", "s000002", "s000003"]
|
||||
importKeys = ["i000004", "i000005", "i000006", "i000007"]
|
||||
@@ -52,16 +53,6 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
|
||||
theItem.setName(123)
|
||||
assert theItem.itemName == ""
|
||||
|
||||
# Handle
|
||||
theItem.setHandle(123)
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abcdef")
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abg")
|
||||
assert theItem.itemHandle is None
|
||||
theItem.setHandle("0123456789abc")
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
|
||||
# Parent
|
||||
theItem.setParent(None)
|
||||
assert theItem.itemParent is None
|
||||
@@ -197,7 +188,7 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
|
||||
theProject = NWProject(mockGUI)
|
||||
mockRnd.reset()
|
||||
buildTestProject(theProject, fncPath)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
|
||||
# Describe Me
|
||||
# ===========
|
||||
@@ -270,24 +261,15 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
|
||||
# ==============
|
||||
|
||||
theItem.setName("New Item")
|
||||
theItem.setHandle("1234567890abc")
|
||||
theItem.setParent("4567890abcdef")
|
||||
assert repr(theItem) == "<NWItem handle=1234567890abc, parent=4567890abcdef, name='New Item'>"
|
||||
theItem.setParent("1111111111111")
|
||||
assert repr(theItem) == "<NWItem handle=0000000000000, parent=1111111111111, name='New Item'>"
|
||||
|
||||
# Truthiness
|
||||
# ==========
|
||||
|
||||
bItem = NWItem(theProject)
|
||||
|
||||
# An item with a handle is valid
|
||||
bItem.setHandle(theProject.tree._makeHandle())
|
||||
assert bool(bItem) is True
|
||||
assert bItem
|
||||
|
||||
# An item without a handle is invalid
|
||||
bItem.setHandle(None)
|
||||
assert bool(bItem) is False
|
||||
assert not bItem
|
||||
# Is True if the handle evaluates to True
|
||||
assert bool(NWItem(theProject, "0000000000000")) is True
|
||||
assert bool(NWItem(theProject, "")) is False
|
||||
|
||||
# Copy an Item
|
||||
# ============
|
||||
@@ -318,29 +300,25 @@ def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
|
||||
}
|
||||
}
|
||||
|
||||
# Get the scene item
|
||||
scItem = theProject.tree[C.hSceneDoc]
|
||||
cpItem = copy.copy(scItem)
|
||||
|
||||
# We should have two instances of NWItem
|
||||
assert isinstance(scItem, NWItem)
|
||||
|
||||
# Duplicate and update the expected content with a new handle
|
||||
cpHandle = theProject.tree._makeHandle()
|
||||
cpData = copy.deepcopy(scData)
|
||||
cpData["itemAttr"]["handle"] = cpHandle
|
||||
|
||||
# Duplicate the scene item
|
||||
cpItem = NWItem.duplicate(scItem, cpHandle)
|
||||
assert isinstance(cpItem, NWItem)
|
||||
assert scItem is not cpItem
|
||||
|
||||
# They should both point to the same project instance
|
||||
assert scItem._project is cpItem._project
|
||||
|
||||
# They should contain the same data
|
||||
# They should contain the same data, except for the handle
|
||||
assert scItem.pack() == scData
|
||||
assert cpItem.pack() == scData
|
||||
|
||||
# Create a new handle for the copy
|
||||
cpHandle = theProject.tree._makeHandle()
|
||||
cpData = copy.deepcopy(scData)
|
||||
cpData["itemAttr"]["handle"] = cpHandle
|
||||
|
||||
# Check that it is indeed changed
|
||||
cpItem.setHandle(cpHandle)
|
||||
assert cpItem.pack() != scData
|
||||
assert cpItem.pack() == cpData
|
||||
|
||||
# Delete the original, and check that the copy remains
|
||||
@@ -356,7 +334,7 @@ def testCoreItem_TypeSetter(mockGUI):
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
|
||||
# Type
|
||||
theItem.setType(None)
|
||||
@@ -385,7 +363,7 @@ def testCoreItem_ClassSetter(mockGUI):
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
|
||||
# Class
|
||||
theItem.setClass(None)
|
||||
@@ -472,7 +450,7 @@ def testCoreItem_LayoutSetter(mockGUI):
|
||||
class.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
|
||||
# Faulty Layouts
|
||||
theItem.setLayout(None)
|
||||
@@ -500,7 +478,7 @@ def testCoreItem_ClassDefaults(mockGUI):
|
||||
"""Test the setter for the default values.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
|
||||
# Root items should not have their class updated
|
||||
theItem.setParent(None)
|
||||
@@ -553,18 +531,17 @@ def testCoreItem_ClassDefaults(mockGUI):
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
|
||||
"""Test packing and unpacking entries for the NWItem class.
|
||||
"""
|
||||
"""Test packing and unpacking entries for the NWItem class."""
|
||||
theProject = NWProject(mockGUI)
|
||||
theProject.data.itemStatus.write(None, "New", (100, 100, 100))
|
||||
theProject.data.itemImport.write(None, "New", (100, 100, 100))
|
||||
|
||||
# Invalid
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "0000000000000")
|
||||
assert theItem.unpack({}) is False
|
||||
|
||||
# File
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "")
|
||||
assert theItem.unpack({
|
||||
"name": "A File",
|
||||
"itemAttr": {
|
||||
@@ -636,7 +613,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
|
||||
}
|
||||
|
||||
# Folder
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "")
|
||||
assert theItem.unpack({
|
||||
"name": "A Folder",
|
||||
"itemAttr": {
|
||||
@@ -701,7 +678,7 @@ def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
|
||||
}
|
||||
|
||||
# Root
|
||||
theItem = NWItem(theProject)
|
||||
theItem = NWItem(theProject, "")
|
||||
assert theItem.unpack({
|
||||
"name": "A Novel",
|
||||
"itemAttr": {
|
||||
|
||||
@@ -25,10 +25,11 @@ from shutil import copyfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mocked import causeOSError
|
||||
from tools import C, cmpFiles, writeFile, buildTestProject, XML_IGNORE
|
||||
from tools import C, cmpFiles, buildTestProject, XML_IGNORE
|
||||
|
||||
from novelwriter import CONFIG
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.tree import NWTree
|
||||
from novelwriter.core.index import NWIndex
|
||||
from novelwriter.core.project import NWProject
|
||||
@@ -58,7 +59,7 @@ def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd):
|
||||
|
||||
assert theProject.projChanged is True
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
copyfile(projFile, testFile)
|
||||
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
|
||||
@@ -154,7 +155,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
|
||||
assert "0000000000011" not in theProject.tree
|
||||
assert "0000000000012" not in theProject.tree
|
||||
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreProject_NewFileFolder
|
||||
|
||||
@@ -172,7 +173,8 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
assert theProject.openProject(fncPath) is False
|
||||
|
||||
# Fail on lock file
|
||||
assert theProject._storage.writeLockFile()
|
||||
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
|
||||
assert theProject.storage.writeLockFile() is True
|
||||
assert theProject.openProject(fncPath) is False
|
||||
assert isinstance(theProject.getLockStatus(), list)
|
||||
|
||||
@@ -182,12 +184,13 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
caplog.clear()
|
||||
assert theProject.openProject(fncPath) is True
|
||||
assert "Failed to check lock file" in caplog.text
|
||||
assert theProject.closeProject()
|
||||
theProject.closeProject()
|
||||
|
||||
# Force open with lockfile
|
||||
assert theProject._storage.writeLockFile()
|
||||
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
|
||||
assert theProject.storage.writeLockFile() is True
|
||||
assert theProject.openProject(fncPath, overrideLock=True) is True
|
||||
assert theProject.closeProject()
|
||||
theProject.closeProject()
|
||||
assert theProject.getLockStatus() is None
|
||||
|
||||
# Fail getting xml reader
|
||||
@@ -237,7 +240,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
mp.setattr("novelwriter.core.tree.NWTree.updateItemData", lambda *a: False)
|
||||
assert theProject.openProject(fncPath) is True
|
||||
|
||||
assert theProject.closeProject()
|
||||
theProject.closeProject()
|
||||
|
||||
# Trigger an index rebuild
|
||||
with monkeypatch.context() as mp:
|
||||
@@ -249,7 +252,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
|
||||
assert "The file format of your project is about to be" in mockGUI.lastQuestion[1]
|
||||
assert theProject.index._indexBroken is False
|
||||
|
||||
assert theProject.closeProject()
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreProject_Open
|
||||
|
||||
@@ -278,7 +281,7 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
|
||||
# Save with and without autosave
|
||||
assert theProject.saveProject(autoSave=False) is True
|
||||
assert theProject.saveProject(autoSave=True) is True
|
||||
assert theProject.closeProject()
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreProject_Save
|
||||
|
||||
@@ -316,7 +319,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
|
||||
C.hWorldRoot,
|
||||
]
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
assert theProject.setTreeOrder(newOrder)
|
||||
theProject.setTreeOrder(newOrder)
|
||||
assert theProject.tree.handles() == newOrder
|
||||
|
||||
# Add a non-existing item
|
||||
@@ -451,7 +454,7 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
|
||||
assert len(theProject.data.itemStatus) == 0
|
||||
assert len(theProject.data.itemImport) == 0
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# END Test testCoreProject_StatusImport
|
||||
|
||||
@@ -509,9 +512,9 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
# Project Language
|
||||
theProject.setProjectChanged(False)
|
||||
theProject.data.setLanguage("en")
|
||||
assert theProject.setProjectLang(None) is True
|
||||
theProject.setProjectLang(None)
|
||||
assert theProject.data.language is None
|
||||
assert theProject.setProjectLang("en_GB") is True
|
||||
theProject.setProjectLang("en_GB")
|
||||
assert theProject.data.language == "en_GB"
|
||||
|
||||
# Language Lookup
|
||||
@@ -562,97 +565,14 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
|
||||
"000000000000e", "000000000000f",
|
||||
]
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
assert theProject.setTreeOrder(newOrder)
|
||||
theProject.setTreeOrder(newOrder)
|
||||
assert theProject.tree.handles() == newOrder
|
||||
assert theProject.setTreeOrder(oldOrder)
|
||||
theProject.setTreeOrder(oldOrder)
|
||||
assert theProject.tree.handles() == oldOrder
|
||||
|
||||
# END Test testCoreProject_Methods
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_OrphanedFiles(mockGUI, prjLipsum):
|
||||
"""Check that files in the content folder that are not tracked in
|
||||
the project XML file are handled correctly by the orphaned files
|
||||
function. It should also restore as much meta data as possible from
|
||||
the meta line at the top of the document file.
|
||||
"""
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
assert theProject.openProject(prjLipsum) is True
|
||||
assert theProject.tree["636b6aa9b697b"] is None
|
||||
|
||||
# Add a file with non-existent parent
|
||||
# This file will be removed from the project on open
|
||||
oHandle = theProject.newFile("Oops", "b3643d0f92e32")
|
||||
theProject.tree[oHandle].setParent("1234567890abc")
|
||||
|
||||
# Save and close
|
||||
assert theProject.saveProject() is True
|
||||
assert theProject.closeProject() is True
|
||||
|
||||
# First Item with Meta Data
|
||||
orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd"
|
||||
writeFile(orphPath, (
|
||||
"%%~name:[Recovered] Mars\n"
|
||||
"%%~path:5eaea4e8cdee8/636b6aa9b697b\n"
|
||||
"%%~kind:WORLD/NOTE\n"
|
||||
"%%~invalid\n"
|
||||
"\n"
|
||||
))
|
||||
|
||||
# Second Item without Meta Data
|
||||
orphPath = prjLipsum / "content" / "736b6aa9b697b.nwd"
|
||||
writeFile(orphPath, "\n")
|
||||
|
||||
# Invalid File Name
|
||||
tstPath = prjLipsum / "content" / "636b6aa9b697b.txt"
|
||||
writeFile(tstPath, "\n")
|
||||
|
||||
# Invalid File Name
|
||||
tstPath = prjLipsum / "content" / "636b6aa9b697bb.nwd"
|
||||
writeFile(tstPath, "\n")
|
||||
|
||||
# Invalid File Name
|
||||
tstPath = prjLipsum / "content" / "abcdefghijklm.nwd"
|
||||
writeFile(tstPath, "\n")
|
||||
|
||||
assert theProject.openProject(prjLipsum)
|
||||
assert theProject.storage.storagePath is not None
|
||||
assert theProject.storage.runtimePath is not None
|
||||
assert theProject.tree["636b6aa9b697bb"] is None
|
||||
assert theProject.tree["abcdefghijklm"] is None
|
||||
|
||||
# First Item with Meta Data
|
||||
oItem = theProject.tree["636b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "[Recovered] Mars"
|
||||
assert oItem.itemHandle == "636b6aa9b697b"
|
||||
assert oItem.itemParent == "60bdf227455cc"
|
||||
assert oItem.itemClass == nwItemClass.WORLD
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
# Second Item without Meta Data
|
||||
oItem = theProject.tree["736b6aa9b697b"]
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Recovered File 1"
|
||||
assert oItem.itemHandle == "736b6aa9b697b"
|
||||
assert oItem.itemParent == "b3643d0f92e32"
|
||||
assert oItem.itemClass == nwItemClass.NOVEL
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
assert theProject.saveProject(prjLipsum)
|
||||
assert theProject.closeProject()
|
||||
|
||||
# Finally, check that the orphaned files function returns
|
||||
# if no project is open and no path is set
|
||||
assert not theProject._scanProjectFolder()
|
||||
|
||||
# END Test testCoreProject_OrphanedFiles
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
|
||||
"""Test the automated backup feature of the project class. The test
|
||||
|
||||
@@ -140,6 +140,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
|
||||
assert xmlReader.state == XMLReadState.PARSED_OK
|
||||
assert xmlReader.xmlRoot == "novelWriterXML"
|
||||
assert xmlReader.xmlVersion == 0x0105
|
||||
assert xmlReader.xmlRevision == 1
|
||||
assert xmlReader.appVersion == "2.0-rc1"
|
||||
assert xmlReader.hexVersion == 0x020000c1
|
||||
|
||||
@@ -213,7 +214,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, tstPaths, fncPath):
|
||||
mockProject = MockProject()
|
||||
mockProject.__setattr__("data", data)
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
packedContent.append(item.pack())
|
||||
|
||||
@@ -333,7 +334,7 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockRnd):
|
||||
mockProject.__setattr__("data", data)
|
||||
status = {}
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
|
||||
packedContent.append(item.pack())
|
||||
@@ -468,7 +469,7 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockRnd):
|
||||
mockProject.__setattr__("data", data)
|
||||
status = {}
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
|
||||
packedContent.append(item.pack())
|
||||
@@ -603,7 +604,7 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockRnd):
|
||||
mockProject.__setattr__("data", data)
|
||||
status = {}
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
|
||||
packedContent.append(item.pack())
|
||||
@@ -741,7 +742,7 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockRnd):
|
||||
mockProject.__setattr__("data", data)
|
||||
status = {}
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
|
||||
packedContent.append(item.pack())
|
||||
@@ -879,7 +880,7 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockRnd):
|
||||
mockProject.__setattr__("data", data)
|
||||
status = {}
|
||||
for entry in content:
|
||||
item = NWItem(mockProject)
|
||||
item = NWItem(mockProject, "0000000000000")
|
||||
item.unpack(entry)
|
||||
status[item.itemHandle] = item.getImportStatus(incIcon=False)[0]
|
||||
packedContent.append(item.pack())
|
||||
|
||||
@@ -63,6 +63,7 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
|
||||
assert storage.getXmlWriter() is None
|
||||
assert bool(storage.getDocument(C.hSceneDoc)) is False
|
||||
assert storage.getMetaFile("file") is None
|
||||
assert storage.scanContent() == []
|
||||
|
||||
# Open project as a new project should fail
|
||||
assert storage.openProjectInPlace(fncPath, newProject=True) is False
|
||||
@@ -90,6 +91,9 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
|
||||
assert isinstance(storage.getXmlReader(), ProjectXMLReader)
|
||||
assert isinstance(storage.getXmlWriter(), ProjectXMLWriter)
|
||||
|
||||
# Get content
|
||||
assert sorted(storage.scanContent()) == [C.hTitlePage, C.hChapterDoc, C.hSceneDoc]
|
||||
|
||||
# Get document
|
||||
assert storage.getDocument(C.hSceneDoc).readDocument() == "### New Scene\n\n"
|
||||
|
||||
@@ -97,7 +101,7 @@ def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
|
||||
assert storage.getMetaFile("stuff") == fncPath / "meta" / "stuff"
|
||||
|
||||
# Clean up
|
||||
assert theProject.closeProject() is True
|
||||
theProject.closeProject()
|
||||
|
||||
# Check closed project return values (again)
|
||||
assert storage.isOpen() is False
|
||||
|
||||
@@ -18,16 +18,18 @@ 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 pytest
|
||||
import random
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from tools import C, buildTestProject
|
||||
from mocked import causeOSError
|
||||
from tools import readFile
|
||||
|
||||
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
|
||||
from novelwriter.common import isHandle
|
||||
from novelwriter.constants import nwFiles
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.core.tree import NWTree
|
||||
@@ -39,20 +41,23 @@ def mockItems(mockGUI, mockRnd):
|
||||
"""Create a list of mock items."""
|
||||
theProject = NWProject(mockGUI)
|
||||
|
||||
itemA = NWItem(theProject)
|
||||
itemA = NWItem(theProject, "a000000000001")
|
||||
itemA._name = "Novel"
|
||||
itemA._parent = None
|
||||
itemA._type = nwItemType.ROOT
|
||||
itemA._class = nwItemClass.NOVEL
|
||||
itemA._expanded = True
|
||||
|
||||
itemB = NWItem(theProject)
|
||||
itemB = NWItem(theProject, "b000000000001")
|
||||
itemB._name = "Act One"
|
||||
itemB._parent = "a000000000001"
|
||||
itemB._type = nwItemType.FOLDER
|
||||
itemB._class = nwItemClass.NOVEL
|
||||
itemB._expanded = True
|
||||
|
||||
itemC = NWItem(theProject)
|
||||
itemC = NWItem(theProject, "c000000000001")
|
||||
itemC._name = "Chapter One"
|
||||
itemC._parent = "b000000000001"
|
||||
itemC._type = nwItemType.FILE
|
||||
itemC._class = nwItemClass.NOVEL
|
||||
itemC._layout = nwItemLayout.DOCUMENT
|
||||
@@ -60,8 +65,9 @@ def mockItems(mockGUI, mockRnd):
|
||||
itemC._wordCount = 50
|
||||
itemC._paraCount = 2
|
||||
|
||||
itemD = NWItem(theProject)
|
||||
itemD = NWItem(theProject, "c000000000002")
|
||||
itemD._name = "Scene One"
|
||||
itemD._parent = "b000000000001"
|
||||
itemD._type = nwItemType.FILE
|
||||
itemD._class = nwItemClass.NOVEL
|
||||
itemD._layout = nwItemLayout.DOCUMENT
|
||||
@@ -69,26 +75,30 @@ def mockItems(mockGUI, mockRnd):
|
||||
itemD._wordCount = 500
|
||||
itemD._paraCount = 20
|
||||
|
||||
itemE = NWItem(theProject)
|
||||
itemE = NWItem(theProject, "a000000000002")
|
||||
itemE._name = "Outtakes"
|
||||
itemE._parent = None
|
||||
itemE._type = nwItemType.ROOT
|
||||
itemE._class = nwItemClass.ARCHIVE
|
||||
itemE._expanded = False
|
||||
|
||||
itemF = NWItem(theProject)
|
||||
itemF = NWItem(theProject, "a000000000003")
|
||||
itemF._name = "Trash"
|
||||
itemF._parent = None
|
||||
itemF._type = nwItemType.ROOT
|
||||
itemF._class = nwItemClass.TRASH
|
||||
itemF._expanded = False
|
||||
|
||||
itemG = NWItem(theProject)
|
||||
itemG = NWItem(theProject, "a000000000004")
|
||||
itemG._name = "Characters"
|
||||
itemG._parent = None
|
||||
itemG._type = nwItemType.ROOT
|
||||
itemG._class = nwItemClass.CHARACTER
|
||||
itemG._expanded = True
|
||||
|
||||
itemH = NWItem(theProject)
|
||||
itemH = NWItem(theProject, "b000000000002")
|
||||
itemH._name = "Jane Doe"
|
||||
itemH._parent = "a000000000004"
|
||||
itemH._type = nwItemType.FILE
|
||||
itemH._class = nwItemClass.CHARACTER
|
||||
itemH._layout = nwItemLayout.NOTE
|
||||
@@ -96,18 +106,7 @@ def mockItems(mockGUI, mockRnd):
|
||||
itemH._wordCount = 400
|
||||
itemH._paraCount = 16
|
||||
|
||||
theItems = [
|
||||
("a000000000001", None, itemA),
|
||||
("b000000000001", "a000000000001", itemB),
|
||||
("c000000000001", "b000000000001", itemC),
|
||||
("c000000000002", "b000000000001", itemD),
|
||||
("a000000000002", None, itemE),
|
||||
("a000000000003", None, itemF),
|
||||
("a000000000004", None, itemG),
|
||||
("b000000000002", "a000000000004", itemH),
|
||||
]
|
||||
|
||||
return theItems
|
||||
return [itemA, itemB, itemC, itemD, itemE, itemF, itemG, itemH]
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
@@ -123,10 +122,10 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
assert theTree.trashRoot() is None
|
||||
|
||||
aHandles = []
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
aHandles.append(tHandle)
|
||||
assert theTree.append(tHandle, pHandle, nwItem) is True
|
||||
assert theTree.updateItemData(tHandle) is True
|
||||
for nwItem in mockItems:
|
||||
aHandles.append(nwItem.itemHandle)
|
||||
assert theTree.append(nwItem) is True
|
||||
assert theTree.updateItemData(nwItem.itemHandle) is True
|
||||
|
||||
assert theTree._treeChanged is True
|
||||
|
||||
@@ -143,6 +142,9 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
for theItem, theHandle in zip(theTree, aHandles):
|
||||
assert theItem.itemHandle == theHandle
|
||||
|
||||
# Trash Folder
|
||||
# ============
|
||||
|
||||
# Check that we have the correct archive and trash folders
|
||||
assert theTree.trashRoot() == "a000000000003"
|
||||
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
|
||||
@@ -157,57 +159,93 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
||||
assert theTree.isTrash("0000000000000") is True # Doesn't exist
|
||||
assert theTree.isTrash("a000000000003") is True # This the trash folder
|
||||
|
||||
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
|
||||
theTree["a000000000003"].setClass(nwItemClass.NO_CLASS) # type: ignore
|
||||
assert theTree.isTrash("a000000000003") is True # This is still trash
|
||||
theTree["a000000000003"].setClass(nwItemClass.TRASH)
|
||||
theTree["a000000000003"].setClass(nwItemClass.TRASH) # type: ignore
|
||||
|
||||
assert theTree.isTrash("b000000000002") is False # This is not trash
|
||||
|
||||
value = theTree["b000000000002"].itemParent
|
||||
theTree["b000000000002"].setParent("a000000000003")
|
||||
value = theTree["b000000000002"].itemParent # type: ignore
|
||||
theTree["b000000000002"].setParent("a000000000003") # type: ignore
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setParent(value)
|
||||
theTree["b000000000002"].setParent(value) # type: ignore
|
||||
|
||||
value = theTree["b000000000002"].itemRoot
|
||||
theTree["b000000000002"].setRoot("a000000000003")
|
||||
value = theTree["b000000000002"].itemRoot # type: ignore
|
||||
theTree["b000000000002"].setRoot("a000000000003") # type: ignore
|
||||
assert theTree.isTrash("b000000000002") is True # This is in trash
|
||||
theTree["b000000000002"].setRoot(value)
|
||||
theTree["b000000000002"].setRoot(value) # type: ignore
|
||||
|
||||
# Try to add another trash folder
|
||||
itemT = NWItem(theProject)
|
||||
itemT = NWItem(theProject, "1111111111111")
|
||||
itemT._name = "Trash"
|
||||
itemT._type = nwItemType.ROOT
|
||||
itemT._class = nwItemClass.TRASH
|
||||
itemT._expanded = False
|
||||
|
||||
assert theTree.append("1234567890abc", None, itemT) is False
|
||||
assert theTree.append(itemT) is False
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
# Generate handle automatically
|
||||
itemT = NWItem(theProject)
|
||||
itemT._name = "New File"
|
||||
itemT._type = nwItemType.FILE
|
||||
itemT._class = nwItemClass.NOVEL
|
||||
itemT._layout = nwItemLayout.DOCUMENT
|
||||
# Create or Add Items
|
||||
# ===================
|
||||
|
||||
assert theTree.append(None, None, itemT) is True
|
||||
assert theTree.updateItemData(itemT.itemHandle) is True
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
# Create a new item, but with invalid parent
|
||||
assert theTree.create("New File", "blabla", nwItemType.FILE, nwItemClass.NO_CLASS) is None
|
||||
|
||||
# Create a new, valid item
|
||||
nHandle = theTree.create("New File", "b000000000001", nwItemType.FILE, nwItemClass.NO_CLASS)
|
||||
assert isHandle(nHandle)
|
||||
assert nHandle == "0000000000000"
|
||||
|
||||
# The new item should be the last item in the tree
|
||||
theList = theTree.handles()
|
||||
nHandle = "0000000000000"
|
||||
assert theList[-1] == nHandle
|
||||
|
||||
# Try to add existing handle
|
||||
assert theTree.append(nHandle, None, itemT) is False
|
||||
# Retrieve the item
|
||||
itemT = theTree[nHandle]
|
||||
assert isinstance(itemT, NWItem)
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
# We should not be allowed to add the item again
|
||||
assert theTree.append(itemT) is False
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
# Create an invalid item to add, which will be rejected
|
||||
itemU = NWItem.duplicate(itemT, "blabla")
|
||||
assert theTree.append(itemU) is False
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
|
||||
# Create a new root, but with a parent set anyway (the parent should be ignored)
|
||||
zHandle = theTree.create("Custom", "a000000000001", nwItemType.ROOT, nwItemClass.CUSTOM)
|
||||
assert isinstance(zHandle, str)
|
||||
itemZ = theTree[zHandle]
|
||||
assert isinstance(itemZ, NWItem)
|
||||
assert itemZ.itemParent is None
|
||||
del theTree[zHandle]
|
||||
|
||||
# Duplicate Items
|
||||
# ===============
|
||||
|
||||
# Duplicate a non-existing item
|
||||
assert theTree.duplicate("blabla") is None
|
||||
|
||||
# Duplicate the new item
|
||||
itemV = theTree.duplicate(nHandle)
|
||||
assert isinstance(itemV, NWItem)
|
||||
assert len(theTree) == len(mockItems) + 2
|
||||
|
||||
dHandle = itemV.itemHandle
|
||||
assert dHandle == "0000000000002"
|
||||
|
||||
# Delete Items
|
||||
# ============
|
||||
|
||||
# Delete a non-existing item
|
||||
del theTree["stuff"]
|
||||
assert len(theTree) == len(mockItems) + 1
|
||||
assert len(theTree) == len(mockItems) + 2
|
||||
|
||||
# Delete the last item
|
||||
# Delete the last items
|
||||
del theTree[nHandle]
|
||||
del theTree[dHandle]
|
||||
assert len(theTree) == len(mockItems)
|
||||
assert nHandle not in theTree
|
||||
|
||||
@@ -235,17 +273,17 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
aHandles = []
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
aHandles.append(tHandle)
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
for nwItem in mockItems:
|
||||
aHandles.append(nwItem.itemHandle)
|
||||
theTree.append(nwItem)
|
||||
theTree.updateItemData(nwItem.itemHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
# Pack
|
||||
tree = theTree.pack()
|
||||
for i, (tHandle, pHandle, nwItem) in enumerate(mockItems):
|
||||
assert tree[i]["itemAttr"]["handle"] == tHandle
|
||||
for i, nwItem in enumerate(mockItems):
|
||||
assert tree[i]["itemAttr"]["handle"] == nwItem.itemHandle
|
||||
|
||||
# Unpack
|
||||
theTree.clear()
|
||||
@@ -257,15 +295,82 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
|
||||
# END Test testCoreTree_PackUnpack
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fncPath, mockRnd):
|
||||
"""Check the project consistency."""
|
||||
theProject = NWProject(mockGUI)
|
||||
buildTestProject(theProject, fncPath)
|
||||
|
||||
# By default, all is well
|
||||
caplog.clear()
|
||||
assert theProject.tree.checkConsistency("Recovered") == (0, 0)
|
||||
assert all(m.endswith("OK") for m in caplog.messages)
|
||||
|
||||
# Give the scene file an unknown parent
|
||||
caplog.clear()
|
||||
theProject.tree[C.hSceneDoc].setParent(C.hInvalid) # type: ignore
|
||||
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
|
||||
assert f"'{C.hSceneDoc}' ... ERROR" in caplog.text
|
||||
|
||||
# The scene file should have been added back to its home
|
||||
itemS = theProject.tree[C.hSceneDoc]
|
||||
assert isinstance(itemS, NWItem)
|
||||
assert itemS.itemParent == C.hChapterDir
|
||||
|
||||
# Create a new file with no meta data, and let the function handle it as orphaned
|
||||
xHandle = "0123456789abc"
|
||||
contentPath = theProject.storage.contentPath
|
||||
assert isinstance(contentPath, Path)
|
||||
assert contentPath == fncPath / "content"
|
||||
(contentPath / f"{xHandle}.nwd").write_text("### Stuff", encoding="utf-8")
|
||||
|
||||
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
|
||||
assert xHandle in theProject.tree
|
||||
itemX = theProject.tree[xHandle]
|
||||
assert isinstance(itemX, NWItem)
|
||||
|
||||
# It should by default be added as a Novel file
|
||||
assert itemX.itemParent == C.hNovelRoot
|
||||
assert itemX.itemRoot == C.hNovelRoot
|
||||
assert itemX.itemClass == nwItemClass.NOVEL
|
||||
assert itemX.itemName == "[Recovered] 0123456789abc"
|
||||
|
||||
# Set an unknown class in the orphaned item
|
||||
itemX.setClass(nwItemClass.OBJECT)
|
||||
itemX.setName("Stuff")
|
||||
itemX.setParent(C.hInvalid)
|
||||
theProject.storage.getDocument(xHandle).writeDocument("### Stuff") # This adds meta data
|
||||
|
||||
# Remove the item in the project, and re-run the consistency check
|
||||
del theProject.tree[xHandle]
|
||||
assert theProject.tree.checkConsistency("Recovered") == (1, 1)
|
||||
assert xHandle in theProject.tree
|
||||
itemX = theProject.tree[xHandle]
|
||||
assert isinstance(itemX, NWItem)
|
||||
|
||||
# It should again be added as a Novel file
|
||||
assert itemX.itemParent == C.hNovelRoot
|
||||
assert itemX.itemRoot == C.hNovelRoot
|
||||
assert itemX.itemClass == nwItemClass.NOVEL
|
||||
assert itemX.itemName == "[Recovered] Stuff"
|
||||
|
||||
# If the tree is empty, there is nowhere to add any of the 4 files
|
||||
theProject.tree.clear()
|
||||
assert theProject.tree.checkConsistency("Recovered") == (4, 0)
|
||||
assert len(theProject.tree) == 0
|
||||
|
||||
# END Test testCoreTree_CheckConsistency
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_Methods(mockGUI, mockItems):
|
||||
"""Test various class methods."""
|
||||
theProject = NWProject(mockGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
for nwItem in mockItems:
|
||||
theTree.append(nwItem)
|
||||
theTree.updateItemData(nwItem.itemHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
@@ -273,22 +378,22 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
assert theTree.updateItemData("stuff") is False
|
||||
|
||||
# Update item data, invalid item parent
|
||||
corrParent = theTree["b000000000001"].itemParent
|
||||
theTree["b000000000001"].setParent("0000000000000")
|
||||
corrParent = theTree["b000000000001"].itemParent # type: ignore
|
||||
theTree["b000000000001"].setParent("0000000000000") # type: ignore
|
||||
assert theTree.updateItemData("b000000000001") is False
|
||||
|
||||
# Update item data, valid item parent
|
||||
theTree["b000000000001"].setParent(corrParent)
|
||||
theTree["b000000000001"].setParent(corrParent) # type: ignore
|
||||
assert theTree.updateItemData("b000000000001") is True
|
||||
|
||||
# Update item data, root is unreachable
|
||||
maxDepth = theTree.MAX_DEPTH
|
||||
theTree.MAX_DEPTH = 0
|
||||
theTree.MAX_DEPTH = 0 # type: ignore
|
||||
with pytest.raises(RecursionError):
|
||||
theTree.updateItemData("b000000000001")
|
||||
theTree.MAX_DEPTH = maxDepth
|
||||
|
||||
# Chech type
|
||||
# Check type
|
||||
assert theTree.checkType("blabla", nwItemType.FILE) is False
|
||||
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
|
||||
assert theTree.checkType("c000000000001", nwItemType.FILE) is True
|
||||
@@ -306,7 +411,7 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
assert roots[3][0] == "a000000000004"
|
||||
|
||||
# Add a fake item to root and check that it can handle it
|
||||
theTree._treeRoots["0000000000000"] = NWItem(theProject)
|
||||
theTree._treeRoots["0000000000000"] = NWItem(theProject, "0000000000000")
|
||||
assert theTree.findRoot(nwItemClass.WORLD) is None
|
||||
del theTree._treeRoots["0000000000000"]
|
||||
|
||||
@@ -318,18 +423,18 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
||||
|
||||
# Cause recursion error
|
||||
maxDepth = theTree.MAX_DEPTH
|
||||
theTree.MAX_DEPTH = 0
|
||||
theTree.MAX_DEPTH = 0 # type: ignore
|
||||
with pytest.raises(RecursionError):
|
||||
theTree.getItemPath("c000000000001")
|
||||
theTree.MAX_DEPTH = maxDepth
|
||||
|
||||
# Break the folder parent handle
|
||||
theTree["b000000000001"]._parent = "stuff"
|
||||
theTree["b000000000001"]._parent = "stuff" # type: ignore
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001"
|
||||
]
|
||||
|
||||
theTree["b000000000001"]._parent = "a000000000001"
|
||||
theTree["b000000000001"]._parent = "a000000000001" # type: ignore
|
||||
assert theTree.getItemPath("c000000000001") == [
|
||||
"c000000000001", "b000000000001", "a000000000001"
|
||||
]
|
||||
@@ -349,13 +454,13 @@ def testCoreTree_MakeHandles(mockGUI):
|
||||
random.seed(42)
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == handles[0]
|
||||
theTree._projTree[handles[0]] = None
|
||||
theTree._projTree[handles[0]] = None # type: ignore
|
||||
|
||||
# Add the next in line to the project to force duplicate
|
||||
theTree._projTree[handles[1]] = None
|
||||
theTree._projTree[handles[1]] = None # type: ignore
|
||||
tHandle = theTree._makeHandle()
|
||||
assert tHandle == handles[2]
|
||||
theTree._projTree[handles[2]] = None
|
||||
theTree._projTree[handles[2]] = None # type: ignore
|
||||
|
||||
# Reset the seed to force collissions, which should still end up
|
||||
# returning the next handle in the sequence
|
||||
@@ -372,8 +477,8 @@ def testCoreTree_Stats(mockGUI, mockItems):
|
||||
theProject = NWProject(mockGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
for nwItem in mockItems:
|
||||
theTree.append(nwItem)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
theTree._treeOrder.append("stuff")
|
||||
@@ -393,9 +498,9 @@ def testCoreTree_Reorder(caplog, mockGUI, mockItems):
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
aHandle = []
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
aHandle.append(tHandle)
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
for nwItem in mockItems:
|
||||
aHandle.append(nwItem.itemHandle)
|
||||
theTree.append(nwItem)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
|
||||
@@ -422,14 +527,14 @@ def testCoreTree_Reorder(caplog, mockGUI, mockItems):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreTree_ToCFile(monkeypatch, tstPaths, mockGUI, mockItems):
|
||||
def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems):
|
||||
"""Test writing the ToC.txt file."""
|
||||
theProject = NWProject(mockGUI)
|
||||
theTree = NWTree(theProject)
|
||||
|
||||
for tHandle, pHandle, nwItem in mockItems:
|
||||
theTree.append(tHandle, pHandle, nwItem)
|
||||
theTree.updateItemData(tHandle)
|
||||
for nwItem in mockItems:
|
||||
theTree.append(nwItem)
|
||||
theTree.updateItemData(nwItem.itemHandle)
|
||||
|
||||
assert len(theTree) == len(mockItems)
|
||||
theTree._treeOrder.append("stuff")
|
||||
@@ -443,24 +548,27 @@ def testCoreTree_ToCFile(monkeypatch, tstPaths, mockGUI, mockItems):
|
||||
return dItem.itemType == nwItemType.FILE
|
||||
|
||||
monkeypatch.setattr("pathlib.Path.is_file", mockIsFile)
|
||||
theProject._storage._runtimePath = fncPath
|
||||
(fncPath / "content").mkdir()
|
||||
|
||||
theProject._storage._runtimePath = None
|
||||
assert theTree.writeToCFile() is False
|
||||
# Block extraction of the path
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("novelwriter.core.storage.NWStorage.contentPath", lambda *a: None)
|
||||
assert theTree.writeToCFile() is False
|
||||
|
||||
theProject._storage._runtimePath = tstPaths.tmpDir
|
||||
# Block opening the file
|
||||
with monkeypatch.context() as mp:
|
||||
mp.setattr("builtins.open", causeOSError)
|
||||
assert theTree.writeToCFile() is False
|
||||
|
||||
theProject._storage._runtimePath = tstPaths.tmpDir
|
||||
(tstPaths.tmpDir / "content").mkdir()
|
||||
# Allow writing
|
||||
assert theTree.writeToCFile() is True
|
||||
|
||||
pathA = str(Path("content") / "c000000000001.nwd")
|
||||
pathB = str(Path("content") / "c000000000002.nwd")
|
||||
pathC = str(Path("content") / "b000000000002.nwd")
|
||||
|
||||
assert readFile(tstPaths.tmpDir / nwFiles.TOC_TXT) == (
|
||||
assert (fncPath / nwFiles.TOC_TXT).read_text() == (
|
||||
"\n"
|
||||
"Table of Contents\n"
|
||||
"=================\n"
|
||||
|
||||
Reference in New Issue
Block a user