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