Remove project content attribute from project class
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -151,6 +151,8 @@ class NWStorage:
|
||||
return xmlWriter
|
||||
|
||||
def getDocument(self, tHandle):
|
||||
"""Return a document wrapper object.
|
||||
"""
|
||||
pass
|
||||
|
||||
def getMetaFile(self, fileName):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user