Use storage class to open and save project XML

This commit is contained in:
Veronica Berglyd Olsen
2022-11-03 21:51:56 +01:00
parent 96b011cf70
commit dfd9e15951
5 changed files with 129 additions and 36 deletions
+32 -9
View File
@@ -105,20 +105,24 @@ class NWProject(QObject):
##
@property
def data(self):
return self._data
def options(self):
return self._options
@property
def index(self):
return self._index
def storage(self):
return self._storage
@property
def data(self):
return self._data
@property
def tree(self):
return self._tree
@property
def options(self):
return self._options
def index(self):
return self._index
@property
def projOpened(self):
@@ -245,6 +249,7 @@ class NWProject(QObject):
self._projAltered = False
# Project Tree
self._storage.clear()
self._tree.clear()
self._index.clearIndex()
self._data = NWProjectData(self)
@@ -302,6 +307,8 @@ class NWProject(QObject):
if not self.setProjectPath(projPath, newProject=True):
return False
self._storage.openProjectInPlace(self.projPath)
self._data.setName(projName)
self._data.setTitle(projTitle)
self._data.setAuthors(projAuthors)
@@ -458,10 +465,18 @@ class NWProject(QObject):
# Open The Project XML File
# =========================
if not self._storage.openProjectInPlace(self.projPath):
self.clearProject()
return False
xmlReader = self._storage.getXmlReader()
if not isinstance(xmlReader, ProjectXMLReader):
self.clearProject()
return False
self._data = NWProjectData(self)
projContent = []
xmlReader = ProjectXMLReader(fileName)
xmlParsed = xmlReader.read(self._data, projContent)
appVersion = xmlReader.appVersion or self.tr("Unknown")
@@ -580,6 +595,12 @@ class NWProject(QObject):
), nwAlert.ERROR)
return False
if not self._storage.isOpen():
self.mainGui.makeAlert(self.tr(
"There is no project open."
), nwAlert.ERROR)
return False
saveTime = time()
if not self.ensureFolderStructure():
return False
@@ -594,11 +615,13 @@ class NWProject(QObject):
self.updateWordCounts()
self.countStatus()
xmlWriter = self._storage.getXmlWriter()
if not isinstance(xmlWriter, ProjectXMLWriter):
return False
saveTime = time()
editTime = int(self._data.editTime + saveTime - self._projOpened)
content = self._tree.pack()
xmlWriter = ProjectXMLWriter(self.projPath)
if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr(
"Failed to save project."
+16 -19
View File
@@ -31,6 +31,7 @@ import novelwriter
from enum import Enum
from lxml import etree
from time import time
from pathlib import Path
from novelwriter.common import (
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
@@ -91,7 +92,7 @@ class ProjectXMLReader:
def __init__(self, path):
self._path = path
self._path = Path(path)
self._state = XMLReadState.NO_ACTION
self._root = ""
@@ -153,7 +154,7 @@ class ProjectXMLReader:
logger.debug("Reading project XML")
try:
xml = etree.parse(self._path)
xml = etree.parse(str(self._path))
self._state = XMLReadState.NO_ERROR
except Exception as exc:
@@ -161,10 +162,10 @@ class ProjectXMLReader:
logger.error("Failed to parse project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
backFile = self._path[:-3]+"bak"
backFile = self._path.with_suffix(".bak")
if os.path.isfile(backFile):
try:
xml = etree.parse(backFile)
xml = etree.parse(str(backFile))
self._state = XMLReadState.PARSED_BACKUP
logger.info("Backup project file parsed")
except Exception as exc:
@@ -445,7 +446,7 @@ class ProjectXMLWriter:
def __init__(self, path):
self._path = path
self._path = Path(path)
self._error = None
return
@@ -514,17 +515,13 @@ class ProjectXMLWriter:
xName.text = item["name"]
# Write the XML tree to file
saveFile = os.path.join(self._path, nwFiles.PROJ_FILE)
tempFile = os.path.join(self._path, nwFiles.PROJ_FILE+"~")
backFile = os.path.join(self._path, nwFiles.PROJ_FILE[:-3]+"bak")
saveFile = self._path / nwFiles.PROJ_FILE
tempFile = saveFile.with_suffix(".tmp")
backFile = saveFile.with_suffix(".bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
xRoot,
pretty_print=True,
encoding="utf-8",
xml_declaration=True
))
tempFile.write_bytes(etree.tostring(
xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
))
except Exception as exc:
self._error = exc
return False
@@ -532,10 +529,10 @@ class ProjectXMLWriter:
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
try:
if os.path.isfile(saveFile):
os.replace(saveFile, backFile)
os.replace(tempFile, saveFile)
except OSError as exc:
if saveFile.exists():
saveFile.replace(backFile)
tempFile.replace(saveFile)
except Exception as exc:
self._error = exc
return False
+78 -6
View File
@@ -25,37 +25,109 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
from pathlib import Path
from novelwriter.constants import nwFiles
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
logger = logging.getLogger(__name__)
class NWStorage:
MODE_INACTIVE = 0
MODE_INPLACE = 1
MODE_ARCHIVE = 2
def __init__(self, theProject):
self.theProject = theProject
self._storagePath = None
self._runtimePath = None
self._openMode = self.MODE_INACTIVE
return
def clear(self):
"""Reset internal variables.
"""
self._storagePath = None
self._runtimePath = None
self._openMode = self.MODE_INACTIVE
return
##
# Core Methods
##
def openProjectFolder(self, path):
pass
def isOpen(self):
"""Check if the storage location is open.
"""
return self._runtimePath is not None
def openProjectInPlace(self, path):
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
inPath = Path(path)
if inPath.is_file():
inPath = inPath.parent
if not inPath.is_dir():
logger.error("No such folder: %s", inPath)
self.clear()
return False
self._storagePath = inPath
self._runtimePath = inPath
self._openMode = self.MODE_INPLACE
return True
def openProjectArchive(self, path):
pass
def close(self):
pass
def runPostSaveTasks(self, autoSave=False):
"""Run tasks after the project has been saved.
"""
if self._openMode == self.MODE_INPLACE:
# Nothing to do, so we just return
return True
return True
def closeSession(self):
"""Run tasks related to closing the session.
"""
# Clear lockfile
self.clear()
return
##
# Content Access Methods
##
def getXmlReader(self):
pass
"""
"""
if self._runtimePath is None:
return None
projFile = self._runtimePath / nwFiles.PROJ_FILE
xmlReader = ProjectXMLReader(projFile)
return xmlReader
def getXmlWriter(self):
pass
"""
"""
if self._runtimePath is None:
return None
xmlWriter = ProjectXMLWriter(self._runtimePath)
return xmlWriter
def getDocument(self, tHandle):
pass
+2 -2
View File
@@ -220,12 +220,12 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, filesDir, fncDir, outDir, refDir
# Fail saving
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
mp.setattr("pathlib.Path.write_bytes", causeOSError)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False
assert str(xmlWriter.error) == "Mock OSError"
with monkeypatch.context() as mp:
mp.setattr("os.replace", causeOSError)
mp.setattr("pathlib.Path.replace", causeOSError)
assert xmlWriter.write(data, packedContent, timeStamp, 1000) is False
assert str(xmlWriter.error) == "Mock OSError"
+1
View File
@@ -167,6 +167,7 @@ def buildTestProject(theObject, projPath):
theProject.clearProject()
theProject.setProjectPath(projPath, newProject=True)
theProject.storage.openProjectInPlace(theProject.projPath)
theProject.data.itemStatus.write(None, "New", (100, 100, 100))
theProject.data.itemStatus.write(None, "Note", (200, 50, 0))