Clean up the content loading

This commit is contained in:
Veronica Berglyd Olsen
2022-11-01 13:36:22 +01:00
parent 0cfaf4f021
commit f41db97860
5 changed files with 101 additions and 100 deletions
+10 -7
View File
@@ -193,21 +193,22 @@ class NWItem:
logger.error("XML item entry does not have a handle")
return False
self.setName(data.get("label", ""))
self.setParent(data.get("parent", None))
self.setRoot(data.get("root", None))
self.setOrder(data.get("order", 0))
self.setType(data.get("type", nwItemType.NO_TYPE))
self.setClass(data.get("class", nwItemClass.NO_CLASS))
self.setLayout(data.get("layout", nwItemLayout.NO_LAYOUT))
self.setExpanded(data.get("expanded", False))
self.setStatus(data.get("status", None))
self.setImport(data.get("import", None))
self.setMainHeading(data.get("heading", "H0"))
self.setCharCount(data.get("charCount", 0))
self.setWordCount(data.get("wordCount", 0))
self.setParaCount(data.get("paraCount", 0))
self.setCursorPos(data.get("cursorPos", 0))
self.setName(data.get("label", ""))
self.setStatus(data.get("status", None))
self.setImport(data.get("import", None))
self.setActive(data.get("active", True))
# Make some checks to ensure consistency
@@ -216,10 +217,12 @@ class NWItem:
self._parent = None # Root items cannot have a parent
if self._type != nwItemType.FILE:
self._charCount = 0 # Only set for files
self._wordCount = 0 # Only set for files
self._paraCount = 0 # Only set for files
self._cursorPos = 0 # Only set for files
self._heading = "H0" # Only files have headers
self._active = False # Can only be True for files
self._charCount = 0 # Only set for files
self._wordCount = 0 # Only set for files
self._paraCount = 0 # Only set for files
self._cursorPos = 0 # Only set for files
return True
+5 -11
View File
@@ -455,13 +455,13 @@ class NWProject(QObject):
# =========================
self._data = NWProjectData(self)
xmlReader = ProjectXMLReader(fileName)
xmlParsed = xmlReader.read(self._data)
projContent = []
xmlReader = ProjectXMLReader(fileName)
xmlParsed = xmlReader.read(self._data, projContent)
nwxRoot = xmlReader.xmlRoot
appVersion = xmlReader.appVersion or self.tr("Unknown")
hexVersion = xmlReader.hexVersion or "0x0"
xmlVersion = xmlReader.xmlVersion or self.tr("Unknown")
if not xmlParsed:
if xmlReader.state == XMLReadState.NOT_NWX_FILE:
@@ -482,9 +482,6 @@ class NWProject(QObject):
self.clearProject()
return False
logger.debug("XML root is '%s'", nwxRoot)
logger.debug("File version is '%s'", xmlVersion)
# Check Legacy Upgrade
# ====================
@@ -522,10 +519,7 @@ class NWProject(QObject):
# Extract Data
# ============
logger.info("Project Name: '%s'", self._data.name)
logger.info("Project Title: '%s'", self._data.title)
self._projTree.unpack(xmlReader.content)
self._projTree.unpack(projContent)
self._optState.loadSettings()
# Sort out old file locations
+53 -56
View File
@@ -30,6 +30,7 @@ import novelwriter
from enum import Enum
from lxml import etree
from time import time
from novelwriter.common import (
checkBool, checkInt, checkStringNone, formatTimeStamp, simplified, checkString
@@ -94,10 +95,6 @@ class ProjectXMLReader:
self._path = path
self._state = XMLReadState.NO_ACTION
self._content = []
self._statusData = {}
self._statusMap = {}
self._root = ""
self._version = 0x0000
self._appVersion = ""
@@ -110,12 +107,6 @@ class ProjectXMLReader:
# Properties
##
@property
def content(self):
"""The project content section, a dictionary of project items.
"""
return self._content
@property
def state(self):
"""The state of the parsing as an XMLReadState enum value.
@@ -156,10 +147,11 @@ class ProjectXMLReader:
# Methods
##
def read(self, projData):
def read(self, projData, projContent):
"""Read and parse the project XML file.
"""
self._content = []
tStart = time()
logger.debug("Reading project XML")
try:
xml = etree.parse(self._path)
@@ -196,6 +188,8 @@ class ProjectXMLReader:
self._state = XMLReadState.UNKNOWN_VERSION
return False
logger.debug("XML is '%s' version '%s'", self._root, fileVersion)
self._appVersion = str(xRoot.attrib.get("appVersion", ""))
self._hexVersion = str(xRoot.attrib.get("appVersion", ""))
self._timeStamp = str(xRoot.attrib.get("timeStamp", ""))
@@ -208,10 +202,9 @@ class ProjectXMLReader:
status &= self._parseProjectSettings(xSection, projData)
elif xSection.tag == "content":
if self._version >= 0x0104:
status &= self._parseProjectContent(xSection)
status &= self._parseProjectContent(xSection, projContent)
else:
self._genLegacyImportStatysMap(projData)
status &= self._parseProjectContentLegacy(xSection)
status &= self._parseProjectContentLegacy(xSection, projContent, projData)
else:
logger.warning("Ignored <root/%s> in xml", xSection.tag)
@@ -224,6 +217,8 @@ class ProjectXMLReader:
else:
self._state = XMLReadState.WAS_LEGACY
logger.debug("Project XML loaded in %.3f ms", (time() - tStart)*1000)
return True
##
@@ -233,7 +228,7 @@ class ProjectXMLReader:
def _parseProjectMeta(self, xSection, projData):
"""Parse the project section of the XML file.
"""
logger.debug("Parsing xml <root/project>")
logger.debug("Parsing <project> section")
for xItem in xSection:
if xItem.tag == "name":
projData.setName(xItem.text)
@@ -255,7 +250,7 @@ class ProjectXMLReader:
def _parseProjectSettings(self, xSection, projData):
"""Parse the settings section of the XML file.
"""
logger.debug("Parsing xml <root/settings>")
logger.debug("Parsing <settings> section")
for xItem in xSection:
if xItem.tag == "doBackup":
@@ -291,53 +286,56 @@ class ProjectXMLReader:
return True
def _parseProjectContent(self, xSection):
def _parseProjectContent(self, xSection, projContent):
"""Parse the content section of the XML file.
"""
logger.debug("Parsing xml <root/content>")
logger.debug("Parsing <content> section")
for xItem in xSection:
if xItem.tag == "item":
item = {}
item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
item["root"] = checkStringNone(xItem.attrib.get("root", None), None)
item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
item["type"] = checkString(xItem.attrib.get("type", "NO_TYPE"), "NO_TYPE")
item["class"] = checkString(xItem.attrib.get("class", "NO_CLASS"), "NO_CLASS")
item["layout"] = checkString(xItem.attrib.get("layout", "NO_LAYOUT"), "NO_LAYOUT")
item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
item["root"] = checkStringNone(xItem.attrib.get("root"), None)
item["order"] = checkInt(xItem.attrib.get("order"), 0)
item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE")
item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS")
item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
for xVal in xItem:
if xVal.tag == "meta":
item["expanded"] = checkBool(xVal.attrib.get("expanded", False), False)
item["heading"] = checkString(xVal.attrib.get("heading", "H0"), "H0")
item["charCount"] = checkInt(xVal.attrib.get("charCount", 0), 0)
item["wordCount"] = checkInt(xVal.attrib.get("wordCount", 0), 0)
item["paraCount"] = checkInt(xVal.attrib.get("paraCount", 0), 0)
item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos", 0), 0)
item["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
item["heading"] = checkString(xVal.attrib.get("heading"), "H0")
item["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
item["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
item["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
item["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
elif xVal.tag == "name":
item["label"] = simplified(checkString(xVal.text, ""))
item["status"] = checkStringNone(xVal.attrib.get("status", None), None)
item["import"] = checkStringNone(xVal.attrib.get("import", None), None)
item["active"] = checkBool(xVal.attrib.get("active", False), False)
item["status"] = checkStringNone(xVal.attrib.get("status"), None)
item["import"] = checkStringNone(xVal.attrib.get("import"), None)
item["active"] = checkBool(xVal.attrib.get("active"), False)
# ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
if "exported" in xVal.attrib:
item["active"] = checkBool(xVal.attrib.get("exported", False), False)
item["active"] = checkBool(xVal.attrib.get("exported"), False)
else:
logger.warning("Ignored <root/content/item/%s> in xml", xVal.tag)
self._content.append(item)
projContent.append(item)
else:
logger.warning("Ignored item <root/content/%s> in xml", xItem.tag)
return True
def _parseProjectContentLegacy(self, xSection):
def _parseProjectContentLegacy(self, xSection, projContent, projData):
"""Parse the content section of the XML file for older versions.
"""
logger.debug("Parsing xml <root/content> (legacy format)")
depLayout = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE")
logger.debug("Parsing <content> section (legacy format)")
# Create maps to look up name -> key for status and importance
statusMap = {entry["name"]: key for key, entry in projData.itemStatus.items()}
importMap = {entry["name"]: key for key, entry in projData.itemImport.items()}
for xItem in xSection:
item = {}
@@ -377,19 +375,21 @@ class ProjectXMLReader:
# Status was split into separate status/import with a key in 1.4
if item.get("class", "") in ("NOVEL", "ARCHIVE"):
item["status"] = self._statusMap.get(tmpStatus, None)
item["status"] = statusMap.get(tmpStatus, None)
else:
item["import"] = self._importMap.get(tmpStatus, None)
item["import"] = importMap.get(tmpStatus, None)
# A number of layouts were removed in 1.3
if item.get("layout", "") in depLayout:
if item.get("layout", "") in (
"TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"
):
item["layout"] = "DOCUMENT"
# The trast type was removed in 1.4
if item.get("type", "") == "TRASH":
item["type"] = "ROOT"
self._content.append(item)
projContent.append(item)
else:
logger.warning("Ignored <root/content/%s> in xml", xItem.tag)
@@ -409,13 +409,6 @@ class ProjectXMLReader:
sObject.write(key, xEntry.text, (red, green, blue), count)
return
def _genLegacyImportStatysMap(self, projData):
"""Generate a map of legacy import/status values.
"""
self._statusMap = {entry["name"]: key for key, entry in projData.itemStatus.items()}
self._importMap = {entry["name"]: key for key, entry in projData.itemImport.items()}
return
def _parseDictKeyText(self, xItem):
"""Parse a dictionary stored with key as an attribute and the
value as the text porperty.
@@ -459,8 +452,10 @@ class ProjectXMLWriter:
def write(self, projData, projContent, saveTime, editTime):
"""Write the project data and content to the XML files.
"""
tStart = time()
logger.debug("Writing project XML")
nwXML = etree.Element("novelWriterXML", attrib={
xRoot = etree.Element("novelWriterXML", attrib={
"appVersion": str(novelwriter.__version__),
"hexVersion": str(novelwriter.__hexversion__),
"fileVersion": FILE_VERSION,
@@ -468,7 +463,7 @@ class ProjectXMLWriter:
})
# Save Project Meta
xProject = etree.SubElement(nwXML, "project")
xProject = etree.SubElement(xRoot, "project")
self._packSingleValue(xProject, "name", projData.name)
self._packSingleValue(xProject, "title", projData.title)
self._packListValue(xProject, "author", projData.authors)
@@ -477,7 +472,7 @@ class ProjectXMLWriter:
self._packSingleValue(xProject, "editTime", editTime)
# Save Project Settings
xSettings = etree.SubElement(nwXML, "settings")
xSettings = etree.SubElement(xRoot, "settings")
self._packSingleValue(xSettings, "doBackup", projData.doBackup)
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellCheck", projData.spellCheck)
@@ -498,7 +493,7 @@ class ProjectXMLWriter:
self._packSingleValue(xImport, "entry", label, attrib=attrib)
# Save Tree Content
xContent = etree.SubElement(nwXML, "content", attrib={"count": str(len(projContent))})
xContent = etree.SubElement(xRoot, "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", {}))
@@ -512,7 +507,7 @@ class ProjectXMLWriter:
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
nwXML,
xRoot,
pretty_print=True,
encoding="utf-8",
xml_declaration=True
@@ -531,6 +526,8 @@ class ProjectXMLWriter:
self._error = exc
return False
logger.debug("Project XML saved in %.3f ms", (time() - tStart)*1000)
return True
##
+8 -11
View File
@@ -205,18 +205,15 @@ class NWStatus:
def pack(self):
"""Pack the status entries into a dictionary.
"""
result = []
for key, data in self._store.items():
result. append((
data["name"], {
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
}
))
return result
yield (data["name"], {
"key": key,
"count": str(data["count"]),
"red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
})
return
def unpack(self, data):
"""Unpack a data dictionary and set the class values.
+25 -15
View File
@@ -54,53 +54,63 @@ def testBaseCommon_CheckStringNone():
@pytest.mark.base
def testBaseCommon_CheckString():
"""Test the checkString function.
"""Test the checkString function. Anything that is a string should
be returned, otherwise it returns the default.
"""
assert checkString("None", "NotNone") == "None"
assert checkString(None, "NotNone") == "NotNone"
assert checkString(1, "NotNone") == "NotNone"
assert checkString(1.0, "NotNone") == "NotNone"
assert checkString(True, "NotNone") == "NotNone"
assert checkString("None", "default") == "None"
assert checkString("Text", "default") == "Text"
assert checkString(None, "default") == "default"
assert checkString(1, "default") == "default"
assert checkString(1.0, "default") == "default"
assert checkString(True, "default") == "default"
# END Test testBaseCommon_CheckString
@pytest.mark.base
def testBaseCommon_CheckInt():
"""Test the checkInt function.
"""Test the checkInt function. Anything that can be converted to an
integer should be returned, otherwise it returns the default.
"""
assert checkInt(None, 3) == 3
assert checkInt("1", 3) == 1
assert checkInt("1.0", 3) == 3
assert checkInt(1, 3) == 1
assert checkInt(1.0, 3) == 1
assert checkInt(True, 3) == 1
assert checkInt(False, 3) == 0
assert checkInt(None, 3) == 3
assert checkInt("1", 3) == 1
assert checkInt("1.0", 3) == 3
# END Test testBaseCommon_CheckInt
@pytest.mark.base
def testBaseCommon_CheckFloat():
"""Test the checkFloat function.
"""Test the checkFloat function. Anything that can be converted to an
integer should be returned, otherwise it returns the default.
"""
assert checkFloat(None, 3.0) == 3.0
assert checkFloat("1", 3.0) == 1.0
assert checkFloat("1.0", 3.0) == 1.0
assert checkFloat(1, 3.0) == 1.0
assert checkFloat(1.0, 3.0) == 1.0
assert checkFloat(True, 3.0) == 1.0
assert checkFloat(False, 3.0) == 0.0
assert checkFloat(None, 3.0) == 3.0
assert checkFloat("1", 3.0) == 1.0
assert checkFloat("1.0", 3.0) == 1.0
# END Test testBaseCommon_CheckInt
@pytest.mark.base
def testBaseCommon_CheckBool():
"""Test the checkBool function.
"""Test the checkBool function. Any bool, string version of Python
bool, or integer 1 or 0, are returned as bool. Otherwise, the
default is returned.
"""
assert checkBool("True", False) is True
assert checkBool("False", True) is False
assert checkBool("Boo", False) is False
assert checkBool("Boo", True) is True
assert checkBool(None, True) is True
assert checkBool(None, False) is False
assert checkBool(0, True) is False
assert checkBool(1, False) is True
assert checkBool(2, True) is True