Remove project content attribute from project class

This commit is contained in:
Veronica Berglyd Olsen
2022-11-06 00:15:24 +01:00
parent 972ed25a8c
commit 76a1421fde
10 changed files with 104 additions and 127 deletions
+33 -23
View File
@@ -23,9 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
from pathlib import Path
from novelwriter.enum import nwItemLayout, nwItemClass from novelwriter.enum import nwItemLayout, nwItemClass
from novelwriter.error import formatException from novelwriter.error import formatException
from novelwriter.common import isHandle, sha256sum from novelwriter.common import isHandle, sha256sum
@@ -73,7 +74,7 @@ class NWDoc:
empty string. If something went wrong, return None. empty string. If something went wrong, return None.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return None return None
@@ -81,17 +82,22 @@ class NWDoc:
logger.error("Unknown novelWriter document") logger.error("Unknown novelWriter document")
return None return None
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return None
docFile = self._docHandle+".nwd" docFile = self._docHandle+".nwd"
logger.debug("Opening document: %s", docFile) logger.debug("Opening document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile) docPath = contentPath / docFile
self._fileLoc = docPath self._fileLoc = docPath
theText = "" theText = ""
self._docMeta = {} self._docMeta = {}
self._prevHash = None self._prevHash = None
if os.path.isfile(docPath): if docPath.exists():
self._prevHash = sha256sum(docPath) self._prevHash = sha256sum(docPath)
try: try:
with open(docPath, mode="r", encoding="utf-8") as inFile: with open(docPath, mode="r", encoding="utf-8") as inFile:
@@ -125,17 +131,20 @@ class NWDoc:
if not. if not.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return False return False
self.theProject.ensureFolderStructure() contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return None
docFile = self._docHandle+".nwd" docFile = self._docHandle+".nwd"
logger.debug("Saving document: %s", docFile) logger.debug("Saving document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile) docPath = contentPath / docFile
docTemp = os.path.join(self.theProject.projContent, docFile+"~") docTemp = docPath.with_suffix(".tmp")
if self._prevHash is not None and not forceWrite: if self._prevHash is not None and not forceWrite:
self._currHash = sha256sum(docPath) self._currHash = sha256sum(docPath)
@@ -164,7 +173,7 @@ class NWDoc:
# If we're here, the file was successfully saved, so we can # If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file # replace the temp file with the actual file
try: try:
os.replace(docTemp, docPath) docTemp.replace(docPath)
except OSError as exc: except OSError as exc:
self._docError = formatException(exc) self._docError = formatException(exc)
return False return False
@@ -179,23 +188,24 @@ class NWDoc:
from the project data folder. from the project data folder.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return False return False
chkList = [ contentPath = self.theProject.storage.contentPath
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"), if not isinstance(contentPath, Path):
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"), logger.error("No content path set")
] return None
for chkFile in chkList: docPath = contentPath / f"{self._docHandle}.nwd"
if os.path.isfile(chkFile): docTemp = docPath.with_suffix(".tmp")
try:
os.unlink(chkFile) try:
logger.debug("Deleted: %s", chkFile) docPath.unlink(missing_ok=True)
except Exception as exc: docTemp.unlink(missing_ok=True)
self._docError = formatException(exc) except Exception as exc:
return False self._docError = formatException(exc)
return False
return True return True
@@ -206,7 +216,7 @@ class NWDoc:
def getFileLocation(self): def getFileLocation(self):
"""Return the file location of the current document. """Return the file location of the current document.
""" """
return self._fileLoc return str(self._fileLoc)
def getCurrentItem(self): def getCurrentItem(self):
"""Return a pointer to the currently open NWItem. """Return a pointer to the currently open NWItem.
+20 -38
View File
@@ -27,12 +27,12 @@ from __future__ import annotations
import os import os
import json import json
from pathlib import Path
import shutil import shutil
import logging import logging
import novelwriter import novelwriter
from time import time from time import time
from pathlib import Path
from functools import partial from functools import partial
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
@@ -84,10 +84,9 @@ class NWProject(QObject):
self.lockedBy = None # Data on which computer has the project open self.lockedBy = None # Data on which computer has the project open
# Class Settings # Class Settings
self.projPath = None # The full path to where the currently open project is saved self.projPath = None # The full path to where the currently open project is saved
self.projContent = None # The full path to the project's content folder self.projDict = None # The spell check dictionary
self.projDict = None # The spell check dictionary self.projFiles = [] # A list of all files in the content folder on load
self.projFiles = [] # A list of all files in the content folder on load
# Internal Mapping # Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -252,10 +251,9 @@ class NWProject(QObject):
self._data = NWProjectData(self) self._data = NWProjectData(self)
# Project Settings # Project Settings
self.projPath = None self.projPath = None
self.projContent = None self.projDict = None
self.projDict = None self.projFiles = []
self.projFiles = []
return return
@@ -271,7 +269,6 @@ class NWProject(QObject):
# ToDo: These should not be set explicitly, and should stay as Path # ToDo: These should not be set explicitly, and should stay as Path
self.projPath = str(self._storage.runtimePath) self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
logger.info("Opening project: %s", self.projPath) logger.info("Opening project: %s", self.projPath)
@@ -468,24 +465,6 @@ class NWProject(QObject):
self.lockedBy = None self.lockedBy = None
return True return True
def ensureFolderStructure(self):
"""Ensure that all necessary folders exist in the project
folder.
"""
if self.projPath is None or self.projPath == "":
return False
self.projContent = os.path.join(self.projPath, "content")
if self.projPath == os.path.expanduser("~"):
# Don't make a mess in the user's home folder
return False
if not self._checkFolder(self.projContent):
return False
return True
def setDefaultStatusImport(self): def setDefaultStatusImport(self):
"""Set the default status and importance values. """Set the default status and importance values.
""" """
@@ -790,31 +769,34 @@ class NWProject(QObject):
orphaned files so the user can either delete them, or put them orphaned files so the user can either delete them, or put them
back into the project tree. back into the project tree.
""" """
if self.projPath is None: contentPath = self._storage.contentPath
if not isinstance(contentPath, Path):
return False return False
# Then check the files in the data folder # Then check the files in the data folder
logger.debug("Checking files in project content folder") logger.debug("Checking files in project content folder")
orphanFiles = [] orphanFiles = []
self.projFiles = [] self.projFiles = []
for fileItem in os.listdir(self.projContent):
if not fileItem.endswith(".nwd"): for item in contentPath.iterdir():
logger.warning("Skipping file: %s", fileItem) itemName = item.name
if not itemName.endswith(".nwd"):
logger.warning("Skipping file: %s", itemName)
continue continue
if len(fileItem) != 17: if len(itemName) != 17:
logger.warning("Skipping file: %s", fileItem) logger.warning("Skipping file: %s", itemName)
continue continue
fHandle = fileItem[:13] fHandle = itemName[:13]
if not isHandle(fHandle): if not isHandle(fHandle):
logger.warning("Skipping file: %s", fileItem) logger.warning("Skipping file: %s", itemName)
continue continue
if fHandle in self._tree: if fHandle in self._tree:
self.projFiles.append(fHandle) self.projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle) logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle)
else: else:
logger.warning("Checking file %s, handle '%s': Orphaned", fileItem, fHandle) logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
orphanFiles.append(fHandle) orphanFiles.append(fHandle)
# Report status # Report status
+2
View File
@@ -151,6 +151,8 @@ class NWStorage:
return xmlWriter return xmlWriter
def getDocument(self, tHandle): def getDocument(self, tHandle):
"""Return a document wrapper object.
"""
pass pass
def getMetaFile(self, fileName): def getMetaFile(self, fileName):
+11 -4
View File
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import random import random
import logging import logging
from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkHandle from novelwriter.common import checkHandle
@@ -140,16 +141,22 @@ class NWTree:
"""Write the convenience table of contents file in the root of """Write the convenience table of contents file in the root of
the project directory. the project directory.
""" """
runtimePath = self.theProject.storage.runtimePath
contentPath = self.theProject.storage.contentPath
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
return False
tocList = [] tocList = []
tocLen = 0 tocLen = 0
for tHandle in self._treeOrder: for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
continue continue
tFile = tHandle+".nwd" tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)): if (contentPath / tFile).is_file():
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format( tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
os.path.join("content", tFile), str(Path("content") / tFile),
tItem.itemClass.name, tItem.itemClass.name,
tItem.itemLayout.name, tItem.itemLayout.name,
tItem.itemName, tItem.itemName,
@@ -159,7 +166,7 @@ class NWTree:
try: try:
# Dump the text # Dump the text
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT) tocText = runtimePath / nwFiles.TOC_TXT
with open(tocText, mode="w", encoding="utf-8") as outFile: with open(tocText, mode="w", encoding="utf-8") as outFile:
outFile.write("\n") outFile.write("\n")
outFile.write("Table of Contents\n") outFile.write("Table of Contents\n")
+1 -3
View File
@@ -163,9 +163,7 @@ class GuiWordList(QDialog):
if item is not None: if item is not None:
outFile.write(item.text() + "\n") outFile.write(item.text() + "\n")
if dctFile.exists(): tmpFile.replace(dctFile)
dctFile.unlink()
tmpFile.rename(dctFile)
except Exception: except Exception:
logger.error("Could not save new word list") logger.error("Could not save new word list")
+7
View File
@@ -64,6 +64,13 @@ def tmpDir():
return theDir return theDir
@pytest.fixture(scope="function")
def tmpPath(tmpDir):
"""A temporary folder for a single test function.
"""
return Path(tmpDir)
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def tstPaths(tmpDir): def tstPaths(tmpDir):
"""Returns an object that can provide the various paths needed for """Returns an object that can provide the various paths needed for
+2 -2
View File
@@ -114,7 +114,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Cause os.replace() to fail while saving # Cause os.replace() to fail while saving
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("os.replace", causeOSError) mp.setattr("pathlib.Path.replace", causeOSError)
assert theDoc.writeDocument(theText) is False assert theDoc.writeDocument(theText) is False
assert theDoc.getError() == "OSError: Mock OSError" assert theDoc.getError() == "OSError: Mock OSError"
@@ -135,7 +135,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Cause the delete to fail # Cause the delete to fail
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError) mp.setattr("pathlib.Path.unlink", causeOSError)
theDoc = NWDoc(theProject, xHandle) theDoc = NWDoc(theProject, xHandle)
assert theDoc.deleteDocument() is False assert theDoc.deleteDocument() is False
assert theDoc.getError() == "OSError: Mock OSError" assert theDoc.getError() == "OSError: Mock OSError"
+1 -31
View File
@@ -141,7 +141,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncDir, outDir, refDir, mockGUI,
# Delete new file, but block access # Delete new file, but block access
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError) mp.setattr("pathlib.Path.unlink", causeOSError)
assert theProject.removeItem("0000000000011") is False assert theProject.removeItem("0000000000011") is False
assert "0000000000011" in theProject.tree assert "0000000000011" in theProject.tree
@@ -270,36 +270,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
# END Test testCoreProject_Save # END Test testCoreProject_Save
@pytest.mark.core
def testCoreProject_Helpers(monkeypatch, fncDir, mockGUI):
"""Test helper functions for the project folder.
"""
theProject = NWProject(mockGUI)
# No path
assert theProject.ensureFolderStructure() is False
# Set the correct dir
theProject.projPath = fncDir
# Block user's home folder
with monkeypatch.context() as mp:
mp.setattr("os.path.expanduser", lambda *a, **k: fncDir)
assert theProject.ensureFolderStructure() is False
# Create a file to block content folder
contentDir = os.path.join(fncDir, "content")
writeFile(contentDir, "stuff")
assert theProject.ensureFolderStructure() is False
os.unlink(contentDir)
# Now, do it right
assert theProject.ensureFolderStructure() is True
assert os.path.isdir(contentDir)
# END Test testCoreProject_Helpers
@pytest.mark.core @pytest.mark.core
def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd): def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd):
"""Test helper functions for the project folder. """Test helper functions for the project folder.
+13 -13
View File
@@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
import random import random
from pathlib import Path
from tools import readFile from tools import readFile
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -394,7 +395,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir): def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpPath):
"""Test writing the ToC.txt file. """Test writing the ToC.txt file.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -411,24 +412,23 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
"""Return True for items that are files in novelWriter and """Return True for items that are files in novelWriter and
should thus also be files in the project folder structure. should thus also be files in the project folder structure.
""" """
dItem = theTree[fileName[8:21]] dItem = theTree[fileName.name[:13]]
assert dItem is not None assert dItem is not None
return dItem.itemType == nwItemType.FILE return dItem.itemType == nwItemType.FILE
monkeypatch.setattr("os.path.isfile", mockIsFile) monkeypatch.setattr("pathlib.Path.is_file", mockIsFile)
theProject.projContent = "content" theProject._storage._runtimePath = None
theProject.projPath = None assert theTree.writeToCFile() is False
assert not theTree.writeToCFile()
theProject.projPath = tmpDir theProject._storage._runtimePath = tmpPath
assert theTree.writeToCFile() assert theTree.writeToCFile() is True
pathA = os.path.join("content", "c000000000001.nwd") pathA = str(Path("content") / "c000000000001.nwd")
pathB = os.path.join("content", "c000000000002.nwd") pathB = str(Path("content") / "c000000000002.nwd")
pathC = os.path.join("content", "b000000000002.nwd") pathC = str(Path("content") / "b000000000002.nwd")
assert readFile(os.path.join(tmpDir, nwFiles.TOC_TXT)) == ( assert readFile(tmpPath / nwFiles.TOC_TXT) == (
"\n" "\n"
"Table of Contents\n" "Table of Contents\n"
"=================\n" "=================\n"
+14 -13
View File
@@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from tools import C, buildTestProject, writeFile from pathlib import Path
from tools import C, buildTestProject
from PyQt5.QtGui import QFocusEvent from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent from PyQt5.QtCore import Qt, QEvent
@@ -46,18 +47,18 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True) nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
writeFile( contentPath = nwGUI.theProject.storage.contentPath
os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"), assert isinstance(contentPath, Path)
"# Jane Doe\n\n@tag: Jane\n\n"
) (contentPath / "0000000000010.nwd").write_text(
writeFile( "# Jane Doe\n\n@tag: Jane\n\n", encoding="utf-8"
os.path.join(nwGUI.theProject.projContent, "000000000000f.nwd"), (
"### Scene One\n\n"
"@pov: Jane\n"
"@focus: Jane\n\n"
"% Synopsis: This is a scene."
)
) )
(contentPath / "000000000000f.nwd").write_text((
"### Scene One\n\n"
"@pov: Jane\n"
"@focus: Jane\n\n"
"% Synopsis: This is a scene."
), encoding="utf-8")
novelView = nwGUI.novelView novelView = nwGUI.novelView
novelTree = novelView.novelTree novelTree = novelView.novelTree