From a88a2f5b4078ab3b620683f69551ba195a41f897 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 31 Oct 2022 22:37:05 +0100 Subject: [PATCH] Change file format to save dictionaries consistently --- novelwriter/core/project.py | 35 +++--- novelwriter/core/projectxml.py | 108 ++++++++++-------- sample/nwProject.nwx | 26 ++--- tests/lipsum/nwProject.nwx | 28 ++--- tests/minimal/nwProject.nwx | 28 ++--- tests/mock.py | 6 +- .../coreProject_NewCustomA_nwProject.nwx | 24 ++-- .../coreProject_NewCustomB_nwProject.nwx | 24 ++-- .../coreProject_NewFileFolder_nwProject.nwx | 24 ++-- .../coreProject_NewMinimal_nwProject.nwx | 24 ++-- .../coreProject_NewRoot_nwProject.nwx | 24 ++-- .../guiEditor_Main_Final_nwProject.nwx | 26 +++-- .../guiEditor_Main_Initial_nwProject.nwx | 24 ++-- 13 files changed, 220 insertions(+), 181 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 3dfecf24..57f9432e 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -24,6 +24,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations + import os import json import shutil @@ -46,7 +48,7 @@ from novelwriter.core.options import OptionState from novelwriter.core.document import NWDoc from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.common import ( - checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, hexToInt, isHandle, + checkBool, checkInt, checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax, simplified, ) @@ -122,7 +124,7 @@ class NWProject(QObject): @property def projChanged(self): - return self._projChanged or self._data.changed + return self._projChanged @property def projAltered(self): @@ -596,7 +598,9 @@ class NWProject(QObject): content = self._projTree.pack() xmlWriter = ProjectXMLWriter(self.projPath) if not xmlWriter.write(self._data, content, saveTime, editTime): - self.mainGui.makeAlert(self.tr("Failed to save project."), nwAlert.ERROR) + self.mainGui.makeAlert(self.tr( + "Failed to save project." + ), nwAlert.ERROR, exception=xmlWriter.error) return False # Save project GUI options @@ -1331,17 +1335,18 @@ class NWProjectData: self._language = None self._spellCheck = False self._spellLang = None - self._lastHandle = { - "editor": "", - "viewer": "", - "novelTree": "", - "outline": "", - } - self._lastCount = {} - self._currCount = {} - self._autoReplace = {} - self._titleFormat = { + # Project Dictionaries + self._lastCount: dict[str, int] = {} + self._currCount: dict[str, int] = {} + self._lastHandle: dict[str, str | None] = { + "editor": None, + "viewer": None, + "novelTree": None, + "outline": None, + } + self._autoReplace: dict[str, str] = {} + self._titleFormat: dict[str, str] = { "title": "%title%", "chapter": "%title%", "unnumbered": "%title%", @@ -1562,12 +1567,12 @@ class NWProjectData: values. """ if isinstance(component, str): - self._lastHandle[component] = checkString(value, "") + self._lastHandle[component] = checkStringNone(value, None) self.theProject.setProjectChanged(True) elif isinstance(value, dict): for key, entry in value.items(): if key in self._lastHandle: - self._lastHandle[key] = checkString(entry, "") + self._lastHandle[key] = str(entry) if isHandle(entry) else None self.theProject.setProjectChanged(True) return diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index e16cca0a..1c3f525d 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -266,11 +266,7 @@ class ProjectXMLReader: projData.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": projData.setSpellLang(xItem.text) - elif xItem.tag == "lastEdited": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "editor") - elif xItem.tag == "lastViewed": # Discontinued in 1.4 - projData.setLastHandle(xItem.text, "viewer") - elif xItem.tag == "lastWordCount": + elif xItem.tag == "totalWordCount": projData.setLastCount(xItem.text, "total") elif xItem.tag == "novelWordCount": projData.setLastCount(xItem.text, "novel") @@ -281,16 +277,30 @@ class ProjectXMLReader: elif xItem.tag in ("import", "importance"): self._parseStatusImport(xItem, projData.itemImport) elif xItem.tag == "lastHandle": - projData.setLastHandle(self._parseDictKeyText(xItem, "component")) + projData.setLastHandle(self._parseDictKeyText(xItem)) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - projData.setAutoReplace(self._parseDictKeyText(xItem, "key")) + projData.setAutoReplace(self._parseDictKeyText(xItem)) else: # Pre 1.2 format projData.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": - projData.setTitleFormat(self._parseDictTagText(xItem)) + if self._version >= 0x0104: + projData.setTitleFormat(self._parseDictKeyText(xItem)) + else: # Pre 1.4 format + projData.setTitleFormat(self._parseDictTagText(xItem)) else: - logger.warning("Ignored in xml", xItem.tag) + if self._version < 0x0104: + # Convert some deprecated fields + if xItem.tag == "lastEdited": # Discontinued in 1.4 + projData.setLastHandle(xItem.text, "editor") + elif xItem.tag == "lastViewed": # Discontinued in 1.4 + projData.setLastHandle(xItem.text, "viewer") + elif xItem.tag == "lastWordCount": # Renamed in 1.4 + projData.setLastCount(xItem.text, "total") + else: + logger.warning("Ignored in xml", xItem.tag) + else: + logger.warning("Ignored in xml", xItem.tag) return True @@ -419,14 +429,14 @@ class ProjectXMLReader: self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()} return - def _parseDictKeyText(self, xItem, keyName): + def _parseDictKeyText(self, xItem): """Parse a dictionary stored with key as an attribute and the value as the text porperty. """ result = {} for xEntry in xItem: - if xEntry.tag == "entry" and keyName in xEntry.attrib: - result[xEntry.attrib[keyName]] = checkString(xEntry.text, "") + if xEntry.tag == "entry" and "key" in xEntry.attrib: + result[xEntry.attrib["key"]] = checkString(xEntry.text, "") return result def _parseDictTagText(self, xItem): @@ -447,7 +457,21 @@ class ProjectXMLWriter: return + ## + # Properties + ## + + @property + def error(self): + return self._error + + ## + # Methods + ## + def write(self, projData, projContent, saveTime, editTime): + """Write the project data and content to the XML files. + """ nwXML = etree.Element("novelWriterXML", attrib={ "appVersion": str(novelwriter.__version__), @@ -471,27 +495,24 @@ class ProjectXMLWriter: self._packSingleValue(xSettings, "language", projData.language) self._packSingleValue(xSettings, "spellCheck", projData.spellCheck) self._packSingleValue(xSettings, "spellLang", projData.spellLang) - self._packSingleValue(xSettings, "lastWordCount", projData.getCurrCount("total")) + self._packSingleValue(xSettings, "totalWordCount", projData.getCurrCount("total")) self._packSingleValue(xSettings, "novelWordCount", projData.getCurrCount("novel")) self._packSingleValue(xSettings, "notesWordCount", projData.getCurrCount("notes")) - self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle, "component") - self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace, "key") - self._packDictTagValue(xSettings, "titleFormat", projData.titleFormat) + self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle) + self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) + self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat) # Save Status/Importance xStatus = etree.SubElement(xSettings, "status") - for (label, attr) in projData.itemStatus.pack(): - xEntry = etree.SubElement(xStatus, "entry", attrib=attr) - xEntry.text = label + for label, attrib in projData.itemStatus.pack(): + self._packSingleValue(xStatus, "entry", label, attrib=attrib) xImport = etree.SubElement(xSettings, "importance") - for (label, attr) in projData.itemImport.pack(): - xEntry = etree.SubElement(xImport, "entry", attrib=attr) - xEntry.text = label + for label, attrib in projData.itemImport.pack(): + self._packSingleValue(xImport, "entry", label, attrib=attrib) # Save Tree Content - cAttr = {"count": str(len(projContent))} - xContent = etree.SubElement(nwXML, "content", attrib=cAttr) + xContent = etree.SubElement(nwXML, "content", attrib={"count": str(len(projContent))}) for item in projContent: xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) @@ -499,8 +520,8 @@ class ProjectXMLWriter: xName.text = item["name"] # Write the xml tree to file - tempFile = os.path.join(self._path, nwFiles.PROJ_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") try: with open(tempFile, mode="wb") as outFile: @@ -510,7 +531,8 @@ class ProjectXMLWriter: encoding="utf-8", xml_declaration=True )) - except Exception: + except Exception as exc: + self._error = exc return False # If we're here, the file was successfully saved, @@ -519,7 +541,8 @@ class ProjectXMLWriter: if os.path.isfile(saveFile): os.replace(saveFile, backFile) os.replace(tempFile, saveFile) - except OSError: + except OSError as exc: + self._error = exc return False return True @@ -528,40 +551,29 @@ class ProjectXMLWriter: # Internal Functions ## - def _packSingleValue(self, xParent, name, value, allowNone=True): - """Pack a list of values into an xml element. + def _packSingleValue(self, xParent, name, value, attrib=None): + """Pack a single value into an xml element. """ - if (value == "" or value is None) and not allowNone: - return - xItem = etree.SubElement(xParent, name) - xItem.text = str(value) + xItem = etree.SubElement(xParent, name, attrib=attrib) + xItem.text = str(value) or "" return - def _packListValue(self, xParent, name, data, allowNone=True): + def _packListValue(self, xParent, name, data): """Pack a list of values into an xml element. """ for value in data: - self._packSingleValue(xParent, name, value, allowNone=allowNone) + xItem = etree.SubElement(xParent, name) + xItem.text = str(value) or "" return - def _packDictKeyValue(self, xParent, name, data, keyName): + def _packDictKeyValue(self, xParent, name, data): """Pack the entries of a dictionary into an xml element. """ xItem = etree.SubElement(xParent, name) for key, value in data.items(): if len(key) > 0: - xEntry = etree.SubElement(xItem, "entry", attrib={keyName: key}) - if value: - xEntry.text = value - return - - def _packDictTagValue(self, xParent, name, data): - """Pack the entries of a dictionary into an xml element. - """ - xItem = etree.SubElement(xParent, name) - for aKey, value in data.items(): - if len(aKey) > 0: - self._packSingleValue(xItem, aKey, value) + xEntry = etree.SubElement(xItem, "entry", attrib={"key": key}) + xEntry.text = str(value) or "" return # END Class ProjectXMLWriter diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index dca0b5a5..d6eeee62 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,27 +1,27 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1403 + 1407 236 - 69454 + 69424 False en_GB True None - 1363 + 1363 954 409 - 636b6aa9b697b - 636b6aa9b697b - 7031beac91f75 - 7031beac91f75 + 636b6aa9b697b + 636b6aa9b697b + 7031beac91f75 + 7031beac91f75 B @@ -29,11 +29,11 @@ D - %title% - Chapter %chw%: %title% - %title% - Scene %ch%.%sc%: %title% -
+ %title% + Chapter %chw%: %title% + %title% + Scene %ch%.%sc%: %title% +
New diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index bcc0b0ce..5ac1b4de 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,35 +1,37 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 28 + 32 24 - 1874 + 1889 False en_GB False None - 7a992350f3eb6 - None - None - None - 3847 + 3847 3109 738 + + 7a992350f3eb6 + None + b3643d0f92e32 + None + Replace Text 1 Replace Text 2 - %title% - Chapter %ch%: %title% - %title% - * * * -
+ %title% + Chapter %ch%: %title% + %title% + * * * +
New diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 3b9d81c6..19689fac 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,33 +1,35 @@ - + Test Minimal Minimal Jane Doe John Doh - 21 + 25 2 - 177 + 203 True en_GB False None - None - None - a508bb932959c - None - 10 + 10 10 0 + + None + None + a508bb932959c + None + - %title% - Chapter %ch%: %title% - %title% - * * * -
+ %title% + Chapter %ch%: %title% + %title% + * * * +
New diff --git a/tests/mock.py b/tests/mock.py index ff67f4bf..74623dd1 100644 --- a/tests/mock.py +++ b/tests/mock.py @@ -19,14 +19,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from PyQt5.QtCore import QObject + # =========================================================================== # # Mock GUI # =========================================================================== # -class MockGuiMain: +class MockGuiMain(QObject): def __init__(self): + super().__init__() + self.mainConf = None self.hasProject = True self.theProject = None diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index a42b8290..69987568 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,20 +14,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 63deaff4..a4a5e280 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -14,20 +14,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 8a390542..e3d19827 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 13 + 13 10 3 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 2bc604a6..7fcc9b61 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project None @@ -12,20 +12,22 @@ None False None - None - None - None - None - 0 + 0 0 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 1614eb63..d256a9a9 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 9 + 9 9 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index ad38f479..69f7208c 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,32 +1,34 @@ - + New Project New Novel Jane Doe 4 2 - 3 + 4 True None True None - 000000000000f - None - 0000000000008 - 0000000000008 - 163 + 163 136 27 + + 000000000000f + None + 0000000000008 + 0000000000008 + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 2e36ef35..9dfb7b59 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project New Novel @@ -13,20 +13,22 @@ None False None - None - None - None - None - 9 + 9 9 0 + + None + None + None + None + - %title% - %title% - %title% - * * * -
+ %title% + %title% + %title% + * * * +
New