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/>.
"""
import os
import logging
from pathlib import Path
from novelwriter.enum import nwItemLayout, nwItemClass
from novelwriter.error import formatException
from novelwriter.common import isHandle, sha256sum
@@ -73,7 +74,7 @@ class NWDoc:
empty string. If something went wrong, return None.
"""
self._docError = ""
if self._docHandle is None:
if not isinstance(self._docHandle, str):
logger.error("No document handle set")
return None
@@ -81,17 +82,22 @@ class NWDoc:
logger.error("Unknown novelWriter document")
return None
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return None
docFile = self._docHandle+".nwd"
logger.debug("Opening document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile)
docPath = contentPath / docFile
self._fileLoc = docPath
theText = ""
self._docMeta = {}
self._prevHash = None
if os.path.isfile(docPath):
if docPath.exists():
self._prevHash = sha256sum(docPath)
try:
with open(docPath, mode="r", encoding="utf-8") as inFile:
@@ -125,17 +131,20 @@ class NWDoc:
if not.
"""
self._docError = ""
if self._docHandle is None:
if not isinstance(self._docHandle, str):
logger.error("No document handle set")
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"
logger.debug("Saving document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
docPath = contentPath / docFile
docTemp = docPath.with_suffix(".tmp")
if self._prevHash is not None and not forceWrite:
self._currHash = sha256sum(docPath)
@@ -164,7 +173,7 @@ class NWDoc:
# If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file
try:
os.replace(docTemp, docPath)
docTemp.replace(docPath)
except OSError as exc:
self._docError = formatException(exc)
return False
@@ -179,23 +188,24 @@ class NWDoc:
from the project data folder.
"""
self._docError = ""
if self._docHandle is None:
if not isinstance(self._docHandle, str):
logger.error("No document handle set")
return False
chkList = [
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
]
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return None
for chkFile in chkList:
if os.path.isfile(chkFile):
try:
os.unlink(chkFile)
logger.debug("Deleted: %s", chkFile)
except Exception as exc:
self._docError = formatException(exc)
return False
docPath = contentPath / f"{self._docHandle}.nwd"
docTemp = docPath.with_suffix(".tmp")
try:
docPath.unlink(missing_ok=True)
docTemp.unlink(missing_ok=True)
except Exception as exc:
self._docError = formatException(exc)
return False
return True
@@ -206,7 +216,7 @@ class NWDoc:
def getFileLocation(self):
"""Return the file location of the current document.
"""
return self._fileLoc
return str(self._fileLoc)
def getCurrentItem(self):
"""Return a pointer to the currently open NWItem.
+20 -38
View File
@@ -27,12 +27,12 @@ from __future__ import annotations
import os
import json
from pathlib import Path
import shutil
import logging
import novelwriter
from time import time
from pathlib import Path
from functools import partial
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
# Class Settings
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.projFiles = [] # A list of all files in the content folder on load
self.projPath = None # The full path to where the currently open project is saved
self.projDict = None # The spell check dictionary
self.projFiles = [] # A list of all files in the content folder on load
# Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -252,10 +251,9 @@ class NWProject(QObject):
self._data = NWProjectData(self)
# Project Settings
self.projPath = None
self.projContent = None
self.projDict = None
self.projFiles = []
self.projPath = None
self.projDict = None
self.projFiles = []
return
@@ -271,7 +269,6 @@ class NWProject(QObject):
# ToDo: These should not be set explicitly, and should stay as Path
self.projPath = str(self._storage.runtimePath)
self.projContent = str(self._storage.contentPath)
logger.info("Opening project: %s", self.projPath)
@@ -468,24 +465,6 @@ class NWProject(QObject):
self.lockedBy = None
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):
"""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
back into the project tree.
"""
if self.projPath is None:
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 fileItem in os.listdir(self.projContent):
if not fileItem.endswith(".nwd"):
logger.warning("Skipping file: %s", fileItem)
for item in contentPath.iterdir():
itemName = item.name
if not itemName.endswith(".nwd"):
logger.warning("Skipping file: %s", itemName)
continue
if len(fileItem) != 17:
logger.warning("Skipping file: %s", fileItem)
if len(itemName) != 17:
logger.warning("Skipping file: %s", itemName)
continue
fHandle = fileItem[:13]
fHandle = itemName[:13]
if not isHandle(fHandle):
logger.warning("Skipping file: %s", fileItem)
logger.warning("Skipping file: %s", itemName)
continue
if fHandle in self._tree:
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:
logger.warning("Checking file %s, handle '%s': Orphaned", fileItem, fHandle)
logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
orphanFiles.append(fHandle)
# Report status
+2
View File
@@ -151,6 +151,8 @@ class NWStorage:
return xmlWriter
def getDocument(self, tHandle):
"""Return a document wrapper object.
"""
pass
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/>.
"""
import os
import random
import logging
from pathlib import Path
from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
@@ -140,16 +141,22 @@ class NWTree:
"""Write the convenience table of contents file in the root of
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 = []
tocLen = 0
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
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(
os.path.join("content", tFile),
str(Path("content") / tFile),
tItem.itemClass.name,
tItem.itemLayout.name,
tItem.itemName,
@@ -159,7 +166,7 @@ class NWTree:
try:
# 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:
outFile.write("\n")
outFile.write("Table of Contents\n")
+1 -3
View File
@@ -163,9 +163,7 @@ class GuiWordList(QDialog):
if item is not None:
outFile.write(item.text() + "\n")
if dctFile.exists():
dctFile.unlink()
tmpFile.rename(dctFile)
tmpFile.replace(dctFile)
except Exception:
logger.error("Could not save new word list")
+7
View File
@@ -64,6 +64,13 @@ def tmpDir():
return theDir
@pytest.fixture(scope="function")
def tmpPath(tmpDir):
"""A temporary folder for a single test function.
"""
return Path(tmpDir)
@pytest.fixture(scope="session")
def tstPaths(tmpDir):
"""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
with monkeypatch.context() as mp:
mp.setattr("os.replace", causeOSError)
mp.setattr("pathlib.Path.replace", causeOSError)
assert theDoc.writeDocument(theText) is False
assert theDoc.getError() == "OSError: Mock OSError"
@@ -135,7 +135,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncDir, mockRnd):
# Cause the delete to fail
with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError)
mp.setattr("pathlib.Path.unlink", causeOSError)
theDoc = NWDoc(theProject, xHandle)
assert theDoc.deleteDocument() is False
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
with monkeypatch.context() as mp:
mp.setattr("os.unlink", causeOSError)
mp.setattr("pathlib.Path.unlink", causeOSError)
assert theProject.removeItem("0000000000011") is False
assert "0000000000011" in theProject.tree
@@ -270,36 +270,6 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncDir, refDir):
# 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
def testCoreProject_AccessItems(mockGUI, fncDir, mockRnd):
"""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/>.
"""
import os
import pytest
import random
from pathlib import Path
from tools import readFile
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@@ -394,7 +395,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core
def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpPath):
"""Test writing the ToC.txt file.
"""
theProject = NWProject(mockGUI)
@@ -411,24 +412,23 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
"""Return True for items that are files in novelWriter and
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
return dItem.itemType == nwItemType.FILE
monkeypatch.setattr("os.path.isfile", mockIsFile)
monkeypatch.setattr("pathlib.Path.is_file", mockIsFile)
theProject.projContent = "content"
theProject.projPath = None
assert not theTree.writeToCFile()
theProject._storage._runtimePath = None
assert theTree.writeToCFile() is False
theProject.projPath = tmpDir
assert theTree.writeToCFile()
theProject._storage._runtimePath = tmpPath
assert theTree.writeToCFile() is True
pathA = os.path.join("content", "c000000000001.nwd")
pathB = os.path.join("content", "c000000000002.nwd")
pathC = os.path.join("content", "b000000000002.nwd")
pathA = str(Path("content") / "c000000000001.nwd")
pathB = str(Path("content") / "c000000000002.nwd")
pathC = str(Path("content") / "b000000000002.nwd")
assert readFile(os.path.join(tmpDir, nwFiles.TOC_TXT)) == (
assert readFile(tmpPath / nwFiles.TOC_TXT) == (
"\n"
"Table of Contents\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/>.
"""
import os
import pytest
from tools import C, buildTestProject, writeFile
from pathlib import Path
from tools import C, buildTestProject
from PyQt5.QtGui import QFocusEvent
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.newTreeItem(nwItemType.FILE)
writeFile(
os.path.join(nwGUI.theProject.projContent, "0000000000010.nwd"),
"# Jane Doe\n\n@tag: Jane\n\n"
)
writeFile(
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 = nwGUI.theProject.storage.contentPath
assert isinstance(contentPath, Path)
(contentPath / "0000000000010.nwd").write_text(
"# Jane Doe\n\n@tag: Jane\n\n", encoding="utf-8"
)
(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
novelTree = novelView.novelTree