diff --git a/novelwriter/common.py b/novelwriter/common.py index 3ccdd751..386681e4 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -1,7 +1,6 @@ """ novelWriter – Common Functions ============================== -Various common functions File History: Created: 2019-05-12 [0.1] @@ -113,9 +112,8 @@ def checkHandle(value, default, allowNone=False): return default -def checkUuid(value, default): - """Try to process a value as an uuid, or return a default. - """ +def checkUuid(value: Any, default: str) -> str: + """Try to process a value as an uuid, or return a default.""" try: return str(uuid.UUID(value)) except Exception: @@ -236,7 +234,7 @@ def formatInt(value): return str(value) -def formatTimeStamp(value, fileSafe=False): +def formatTimeStamp(value: float, fileSafe: bool = False) -> str: """Take a number (on the format returned by time.time()) and convert it to a timestamp string. """ @@ -246,7 +244,7 @@ def formatTimeStamp(value, fileSafe=False): return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP) -def formatTime(t): +def formatTime(t: int) -> str: """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format if a full day or longer. """ diff --git a/novelwriter/core/projectdata.py b/novelwriter/core/projectdata.py index ad376cd1..50c48086 100644 --- a/novelwriter/core/projectdata.py +++ b/novelwriter/core/projectdata.py @@ -1,7 +1,6 @@ """ novelWriter – Project Data Class ================================ -Data class for novelWriter projects File History: Created: 2022-10-30 [2.0rc2] @@ -27,6 +26,8 @@ from __future__ import annotations import uuid import logging +from typing import Any + from novelwriter.common import ( checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified ) @@ -36,6 +37,11 @@ logger = logging.getLogger(__name__) class NWProjectData: + """Core: Project Data Class + + The class holds all project data from the main XML file, aside from + the list of project items. + """ def __init__(self, theProject): @@ -84,75 +90,99 @@ class NWProjectData: ## @property - def uuid(self): + def uuid(self) -> str: + """Return the project ID.""" return self._uuid @property - def name(self): + def name(self) -> str: + """Return the project name.""" return self._name @property - def title(self): + def title(self) -> str: + """Return the project title.""" return self._title @property - def author(self): + def author(self) -> str: + """Return the project author.""" return self._author @property - def saveCount(self): + def saveCount(self) -> int: + """Return the count of project saves.""" return self._saveCount @property - def autoCount(self): + def autoCount(self) -> int: + """Return the count of project auto-saves.""" return self._autoCount @property - def editTime(self): + def editTime(self) -> int: + """Return the number of seconds the project has been edited.""" return self._editTime @property - def doBackup(self): + def doBackup(self) -> bool: + """Return the backup setting.""" return self._doBackup @property - def language(self): + def language(self) -> str | None: + """Return the project language setting.""" return self._language @property - def spellCheck(self): + def spellCheck(self) -> bool: + """Return the spell check enabled setting.""" return self._spellCheck @property - def spellLang(self): + def spellLang(self) -> str | None: + """Return the spell check language.""" return self._spellLang @property - def initCounts(self): + def initCounts(self) -> tuple[int, int]: + """Return the initial count of words for novel and note + documents. + """ return tuple(self._initCounts) @property - def currCounts(self): + def currCounts(self) -> tuple[int, int]: + """Return the current count of words for novel and note + documents. + """ return tuple(self._currCounts) @property - def lastHandle(self): + def lastHandle(self) -> dict[str, str | None]: + """Return the dictionary of last used handles for various + components of the GUI. + """ return self._lastHandle @property - def autoReplace(self): + def autoReplace(self) -> dict[str, str]: + """Return the autoreplace dictionary.""" return self._autoReplace @property def titleFormat(self): + """Delete""" return self._titleFormat @property - def itemStatus(self): + def itemStatus(self) -> NWStatus: + """Return the status settings object.""" return self._status @property - def itemImport(self): + def itemImport(self) -> NWStatus: + """Return the importance settings object.""" return self._import ## @@ -160,15 +190,13 @@ class NWProjectData: ## def incSaveCount(self): - """Increment the save count by one. - """ + """Increment the save count by one.""" self._saveCount += 1 self.theProject.setProjectChanged(True) return def incAutoCount(self): - """Increment the auto save count by one. - """ + """Increment the auto save count by one.""" self._autoCount += 1 self.theProject.setProjectChanged(True) return @@ -177,23 +205,20 @@ class NWProjectData: # Getters ## - def getLastHandle(self, component): - """Retrieve the last used handle for a given component. - """ + def getLastHandle(self, component: str) -> str | None: + """Retrieve the last used handle for a given component.""" return self._lastHandle.get(component, None) def getTitleFormat(self, kind): - """Retrieve the title format string for a given kind of header. - """ + """Retrieve the title format string for a given kind of header.""" return self._titleFormat.get(kind, "%title%") ## # Setters ## - def setUuid(self, value): - """Set the project id. - """ + def setUuid(self, value: Any): + """Set the project id.""" value = checkUuid(value, "") if not value: self._uuid = str(uuid.uuid4()) @@ -202,84 +227,74 @@ class NWProjectData: self.theProject.setProjectChanged(True) return - def setName(self, value): - """Set a new project name. - """ + def setName(self, value: str | None): + """Set a new project name.""" if value != self._name: self._name = simplified(str(value or "")) self.theProject.setProjectChanged(True) return - def setTitle(self, value): - """Set a new novel title. - """ + def setTitle(self, value: str | None): + """Set a new novel title.""" if value != self._title: self._title = simplified(str(value or "")) self.theProject.setProjectChanged(True) return - def setAuthor(self, value): - """Set the author value. - """ + def setAuthor(self, value: str | None): + """Set the author value.""" if value != self._title: self._author = simplified(str(value or "")) self.theProject.setProjectChanged(True) return - def setSaveCount(self, value): - """Set the save count from last session. - """ + def setSaveCount(self, value: Any): + """Set the save count from last session.""" self._saveCount = checkInt(value, 0) self.theProject.setProjectChanged(True) return - def setAutoCount(self, value): - """Set the auto save count from last session. - """ + def setAutoCount(self, value: Any): + """Set the auto save count from last session.""" self._autoCount = checkInt(value, 0) self.theProject.setProjectChanged(True) return - def setEditTime(self, value): - """Set tyje edit time from last session. - """ + def setEditTime(self, value: Any): + """Set tyje edit time from last session.""" self._editTime = checkInt(value, 0) self.theProject.setProjectChanged(True) return - def setDoBackup(self, value): - """Set the do write backup flag. - """ + def setDoBackup(self, value: Any): + """Set the do write backup flag.""" if value != self._doBackup: self._doBackup = checkBool(value, False) self.theProject.setProjectChanged(True) return - def setLanguage(self, value): - """Set the project language. - """ + def setLanguage(self, value: str | None): + """Set the project language.""" if value != self._language: self._language = checkStringNone(value, None) self.theProject.setProjectChanged(True) return - def setSpellCheck(self, value): - """Set the spell check flag. - """ + def setSpellCheck(self, value: Any): + """Set the spell check flag.""" if value != self._spellCheck: self._spellCheck = checkBool(value, False) self.theProject.setProjectChanged(True) return - def setSpellLang(self, value): - """Set the spell check language. - """ + def setSpellLang(self, value: str | None): + """Set the spell check language.""" if value != self._spellLang: self._spellLang = checkStringNone(value, None) self.theProject.setProjectChanged(True) return - def setLastHandle(self, value, component=None): + def setLastHandle(self, value: dict, component: str | None = None): """Set a last used handle into the handle registry. If component is None, the value is assumed to be the whole dictionary of values. @@ -294,9 +309,8 @@ class NWProjectData: self.theProject.setProjectChanged(True) return - def setInitCounts(self, novel=None, notes=None): - """Set the worc count totals for novel and note files. - """ + def setInitCounts(self, novel: Any = None, notes: Any = None): + """Set the word count totals for novel and note files.""" if novel is not None: self._initCounts[0] = checkInt(novel, 0) self._currCounts[0] = checkInt(novel, 0) @@ -305,18 +319,16 @@ class NWProjectData: self._currCounts[1] = checkInt(notes, 0) return - def setCurrCounts(self, novel=None, notes=None): - """Set the worc count totals for novel and note files. - """ + def setCurrCounts(self, novel: Any = None, notes: Any = None): + """Set the word count totals for novel and note files.""" if novel is not None: self._currCounts[0] = checkInt(novel, 0) if notes is not None: self._currCounts[1] = checkInt(notes, 0) return - def setAutoReplace(self, value): - """Set the auto-replace dictionary. - """ + def setAutoReplace(self, value: dict): + """Set the auto-replace dictionary.""" if isinstance(value, dict): self._autoReplace = {} for key, entry in value.items(): @@ -326,8 +338,7 @@ class NWProjectData: return def setTitleFormat(self, value): - """Set the title formats. - """ + """Set the title formats.""" if isinstance(value, dict): for key, entry in value.items(): if key in self._titleFormat and isinstance(entry, str): diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index b39356a2..0726d7da 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -1,7 +1,6 @@ """ novelWriter – Project XML Read/Write ==================================== -Classes for reading and writing the project XML file File History: Created: 2022-09-28 [2.0rc2] XMLReadState @@ -24,6 +23,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import logging import xml.etree.ElementTree as ET @@ -31,6 +31,7 @@ import xml.etree.ElementTree as ET from enum import Enum from time import time from pathlib import Path +from typing import TYPE_CHECKING from novelwriter import __version__, __hexversion__ from novelwriter.common import ( @@ -39,6 +40,10 @@ from novelwriter.common import ( ) from novelwriter.constants import nwFiles +if TYPE_CHECKING: + from novelwriter.core.status import NWStatus + from novelwriter.core.projectdata import NWProjectData + logger = logging.getLogger(__name__) FILE_VERSION = "1.5" # The current project file format version @@ -55,6 +60,7 @@ NUM_VERSION = { class XMLReadState(Enum): + """The state of an XML read process.""" NO_ACTION = 0 NO_ERROR = 1 @@ -69,8 +75,10 @@ class XMLReadState(Enum): class ProjectXMLReader: - """The main project XML file reader class. All data is read into a - NWProjectData instance, which must be provided. + """Core: Project XML Reader + + All data is read into a NWProjectData instance, which must be + provided. File Format Version Change History ================================== @@ -100,16 +108,13 @@ class ProjectXMLReader: """ def __init__(self, path): - self._path = Path(path) self._state = XMLReadState.NO_ACTION - self._root = "" self._version = 0x0 self._appVersion = "" self._hexVersion = 0x0 self._timeStamp = "" - return ## @@ -117,55 +122,46 @@ class ProjectXMLReader: ## @property - def state(self): - """The state of the parsing as an XMLReadState enum value. - """ + def state(self) -> XMLReadState: + """Return the parsing state.""" return self._state @property - def xmlRoot(self): - """The root tag name of the XNL file, - """ + def xmlRoot(self) -> str: + """Return the root tag name of the XNL file.""" return self._root @property - def xmlVersion(self): - """The project XML version number. - """ + def xmlVersion(self) -> int: + """Return the project XML version number.""" return self._version @property - def appVersion(self): - """The novelWriter version number who wrote the file. - """ + def appVersion(self) -> str: + """Return the version number who wrote the file.""" return self._appVersion @property - def hexVersion(self): - """The novelWriter version number who wrote the file as hex. - """ + def hexVersion(self) -> int: + """Return the version number who wrote the file as hex.""" return self._hexVersion @property - def timeStamp(self): - """The date and time when the file was written. - """ + def timeStamp(self) -> str: + """Return the date and time when the file was written.""" return self._timeStamp ## # Methods ## - def read(self, projData, projContent): - """Read and parse the project XML file. - """ + def read(self, data: NWProjectData, content: list) -> bool: + """Read and parse the project XML file.""" tStart = time() logger.debug("Reading project XML") - try: xml = ET.parse(str(self._path)) self._state = XMLReadState.NO_ERROR - except Exception as exc: # Trying to open backup file instead logger.error("Failed to parse project XML", exc_info=exc) @@ -206,14 +202,14 @@ class ProjectXMLReader: for xSection in xRoot: if xSection.tag == "project": - self._parseProjectMeta(xSection, projData) + self._parseProjectMeta(xSection, data) elif xSection.tag == "settings": - self._parseProjectSettings(xSection, projData) + self._parseProjectSettings(xSection, data) elif xSection.tag == "content": if self._version >= 0x0104: - self._parseProjectContent(xSection, projData, projContent) + self._parseProjectContent(xSection, data, content) else: - self._parseProjectContentLegacy(xSection, projData, projContent) + self._parseProjectContentLegacy(xSection, data, content) else: logger.warning("Ignored in XML", xSection.tag) @@ -230,23 +226,22 @@ class ProjectXMLReader: # Internal Functions ## - def _parseProjectMeta(self, xSection, projData): - """Parse the project section of the XML file. - """ + def _parseProjectMeta(self, xSection: ET.Element, data: NWProjectData): + """Parse the project section of the XML file.""" logger.debug("Parsing section") - projData.setUuid(xSection.attrib.get("id", None)) # Added in 1.5 - projData.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5 - projData.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5 - projData.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5 + data.setUuid(xSection.attrib.get("id", None)) # Added in 1.5 + data.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5 + data.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5 + data.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5 for xItem in xSection: if xItem.tag == "name": - projData.setName(xItem.text) + data.setName(xItem.text) elif xItem.tag == "title": - projData.setTitle(xItem.text) + data.setTitle(xItem.text) elif xItem.tag == "author": - projData.setAuthor(xItem.text) + data.setAuthor(xItem.text) else: logger.warning("Ignored in XML", xItem.tag) @@ -254,43 +249,42 @@ class ProjectXMLReader: if self._version < HEX_VERSION: for xItem in xSection: if xItem.tag == "saveCount": # Moved to attribute in 1.5 - projData.setSaveCount(xItem.text) + data.setSaveCount(xItem.text) elif xItem.tag == "autoCount": # Moved to attribute in 1.5 - projData.setAutoCount(xItem.text) + data.setAutoCount(xItem.text) elif xItem.tag == "editTime": # Moved to attribute in 1.5 - projData.setEditTime(xItem.text) + data.setEditTime(xItem.text) return - def _parseProjectSettings(self, xSection, projData): - """Parse the settings section of the XML file. - """ + def _parseProjectSettings(self, xSection: ET.Element, data: NWProjectData): + """Parse the settings section of the XML file.""" logger.debug("Parsing section") for xItem in xSection: if xItem.tag == "doBackup": - projData.setDoBackup(xItem.text) + data.setDoBackup(xItem.text) elif xItem.tag == "language": - projData.setLanguage(xItem.text) + data.setLanguage(xItem.text) elif xItem.tag == "spellChecking": - projData.setSpellLang(xItem.text) - projData.setSpellCheck(xItem.attrib.get("auto", False)) + data.setSpellLang(xItem.text) + data.setSpellCheck(xItem.attrib.get("auto", False)) elif xItem.tag == "status": - self._parseStatusImport(xItem, projData.itemStatus) + self._parseStatusImport(xItem, data.itemStatus) elif xItem.tag == "importance": - self._parseStatusImport(xItem, projData.itemImport) + self._parseStatusImport(xItem, data.itemImport) elif xItem.tag == "lastHandle": - projData.setLastHandle(self._parseDictKeyText(xItem)) + data.setLastHandle(self._parseDictKeyText(xItem)) elif xItem.tag == "autoReplace": if self._version >= 0x0102: - projData.setAutoReplace(self._parseDictKeyText(xItem)) + data.setAutoReplace(self._parseDictKeyText(xItem)) else: # Pre 1.2 format - projData.setAutoReplace(self._parseDictTagText(xItem)) + data.setAutoReplace(self._parseDictTagText(xItem)) elif xItem.tag == "titleFormat": if self._version >= 0x0105: - projData.setTitleFormat(self._parseDictKeyText(xItem)) + data.setTitleFormat(self._parseDictKeyText(xItem)) else: # Pre 1.4 format - projData.setTitleFormat(self._parseDictTagText(xItem)) + data.setTitleFormat(self._parseDictTagText(xItem)) else: logger.warning("Ignored in XML", xItem.tag) @@ -298,23 +292,22 @@ class ProjectXMLReader: if self._version < HEX_VERSION: for xItem in xSection: if xItem.tag == "spellCheck": # Changed to spellChecking in 1.5 - projData.setSpellCheck(xItem.text) + data.setSpellCheck(xItem.text) elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5 - projData.setSpellLang(xItem.text) + data.setSpellLang(xItem.text) elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5 - projData.setInitCounts(novel=xItem.text) + data.setInitCounts(novel=xItem.text) elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5 - projData.setInitCounts(notes=xItem.text) + data.setInitCounts(notes=xItem.text) return - def _parseProjectContent(self, xSection, projData, projContent): - """Parse the content section of the XML file. - """ + def _parseProjectContent(self, xSection: ET.Element, data: NWProjectData, content: list): + """Parse the content section of the XML file.""" logger.debug("Parsing section") - projData.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5 - projData.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5 + data.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5 + data.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5 for xItem in xSection: if xItem.tag != "item": @@ -355,7 +348,7 @@ class ProjectXMLReader: if xVal.tag == "name" and "exported" in xVal.attrib: name["active"] = checkBool(xVal.attrib.get("exported"), False) - projContent.append({ + content.append({ "name": itemName, "itemAttr": item, "metaAttr": meta, @@ -364,14 +357,13 @@ class ProjectXMLReader: return - def _parseProjectContentLegacy(self, xSection, projData, projContent): - """Parse the content section of the XML file for older versions. - """ + def _parseProjectContentLegacy(self, xSection: ET.Element, data: NWProjectData, content: list): + """Parse the content section of the XML file for older versions.""" logger.debug("Parsing section (legacy format)") # Create maps to look up name -> key for status and importance - statusMap = {entry.get("name"): key for key, entry in projData.itemStatus.items()} - importMap = {entry.get("name"): key for key, entry in projData.itemImport.items()} + statusMap = {entry.get("name"): key for key, entry in data.itemStatus.items()} + importMap = {entry.get("name"): key for key, entry in data.itemImport.items()} for xItem in xSection: if xItem.tag != "item": @@ -432,7 +424,7 @@ class ProjectXMLReader: if item.get("type", "") == "TRASH": item["type"] = "ROOT" - projContent.append({ + content.append({ "name": itemName, "itemAttr": item, "metaAttr": meta, @@ -441,9 +433,8 @@ class ProjectXMLReader: return - def _parseStatusImport(self, xItem, sObject): - """Parse a status or importance entry. - """ + def _parseStatusImport(self, xItem: ET.Element, sObject: NWStatus): + """Parse a status or importance entry.""" for xEntry in xItem: if xEntry.tag == "entry": key = xEntry.attrib.get("key", None) @@ -454,7 +445,7 @@ class ProjectXMLReader: sObject.write(key, xEntry.text, (red, green, blue), count) return - def _parseDictKeyText(self, xItem): + def _parseDictKeyText(self, xItem: ET.Element) -> dict: """Parse a dictionary stored with key as an attribute and the value as the text porperty. """ @@ -474,12 +465,15 @@ class ProjectXMLReader: class ProjectXMLWriter: + """Core: Project XML Writer + + The project writer class will only write a file according to the + very latest spec. + """ def __init__(self, path): - self._path = Path(path) self._error = None - return ## @@ -487,16 +481,16 @@ class ProjectXMLWriter: ## @property - def error(self): + def error(self) -> Exception | None: + """Return the error status.""" return self._error ## # Methods ## - def write(self, projData, projContent, saveTime, editTime): - """Write the project data and content to the XML files. - """ + def write(self, data: NWProjectData, content: list, saveTime: float, editTime: int) -> bool: + """Write the project data and content to the XML files.""" tStart = time() logger.debug("Writing project XML") @@ -509,46 +503,46 @@ class ProjectXMLWriter: # Save Project Meta projAttr = { - "id": projData.uuid, - "saveCount": str(projData.saveCount), - "autoCount": str(projData.autoCount), + "id": data.uuid, + "saveCount": str(data.saveCount), + "autoCount": str(data.autoCount), "editTime": str(editTime), } xProject = ET.SubElement(xRoot, "project", attrib=projAttr) - self._packSingleValue(xProject, "name", projData.name) - self._packSingleValue(xProject, "title", projData.title) - self._packSingleValue(xProject, "author", projData.author) + self._packSingleValue(xProject, "name", data.name) + self._packSingleValue(xProject, "title", data.title) + self._packSingleValue(xProject, "author", data.author) # Save Project Settings xSettings = ET.SubElement(xRoot, "settings") - self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup)) - self._packSingleValue(xSettings, "language", projData.language) - self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={ - "auto": yesNo(projData.spellCheck) + self._packSingleValue(xSettings, "doBackup", yesNo(data.doBackup)) + self._packSingleValue(xSettings, "language", data.language) + self._packSingleValue(xSettings, "spellChecking", data.spellLang, attrib={ + "auto": yesNo(data.spellCheck) }) - self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle) - self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace) - self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat) + self._packDictKeyValue(xSettings, "lastHandle", data.lastHandle) + self._packDictKeyValue(xSettings, "autoReplace", data.autoReplace) + self._packDictKeyValue(xSettings, "titleFormat", data.titleFormat) # Save Status/Importance xStatus = ET.SubElement(xSettings, "status") - for label, attrib in projData.itemStatus.pack(): + for label, attrib in data.itemStatus.pack(): self._packSingleValue(xStatus, "entry", label, attrib=attrib) xImport = ET.SubElement(xSettings, "importance") - for label, attrib in projData.itemImport.pack(): + for label, attrib in data.itemImport.pack(): self._packSingleValue(xImport, "entry", label, attrib=attrib) # Save Tree Content contAttr = { - "items": str(len(projContent)), - "novelWords": str(projData.currCounts[0]), - "notesWords": str(projData.currCounts[1]), + "items": str(len(content)), + "novelWords": str(data.currCounts[0]), + "notesWords": str(data.currCounts[1]), } xContent = ET.SubElement(xRoot, "content", attrib=contAttr) - for item in projContent: + for item in content: xItem = ET.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) ET.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) xName = ET.SubElement(xItem, "name", attrib=item.get("nameAttr", {})) @@ -584,16 +578,16 @@ class ProjectXMLWriter: # Internal Functions ## - def _packSingleValue(self, xParent, name, value, attrib=None): - """Pack a single value into an XML element. - """ + def _packSingleValue( + self, xParent: ET.Element, name: str, value: str | None, attrib: dict | None = None + ): + """Pack a single value into an XML element.""" xItem = ET.SubElement(xParent, name, attrib=attrib or {}) xItem.text = str(value) or "" return - def _packDictKeyValue(self, xParent, name, data): - """Pack the entries of a dictionary into an XML element. - """ + def _packDictKeyValue(self, xParent: ET.Element, name: str, data: dict): + """Pack the entries of a dictionary into an XML element.""" xItem = ET.SubElement(xParent, name) for key, value in data.items(): if len(key) > 0: