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