From 59931c804c342c31fb117766ecd4c459d22c3ac6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 29 May 2023 17:00:52 +0200 Subject: [PATCH] Replace lxml with standard xml for project files --- novelwriter/common.py | 45 ++++++++++++++++++++++++ novelwriter/config.py | 1 - novelwriter/core/projectxml.py | 42 ++++++++++++----------- novelwriter/guimain.py | 9 ++--- sample/nwProject.nwx | 62 +++++++++++++++++----------------- 5 files changed, 104 insertions(+), 55 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 91058fd0..f84f22ac 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -27,6 +27,7 @@ import json import uuid import hashlib import logging +import xml.etree.ElementTree as ET from pathlib import Path from datetime import datetime @@ -562,3 +563,47 @@ class NWConfigParser(ConfigParser): return result # END Class NWConfigParser + + +# =============================================================================================== # +# Third Party Code +# =============================================================================================== # + +def xmlIndent(tree, space=" ", level=0): + """The XML indent function from CPython, that was only added in Python 3.9. + It is included here to support older versions of Python. + https://github.com/python/cpython/blob/main/Lib/xml/etree/ElementTree.py + """ + if isinstance(tree, ET.ElementTree): + tree = tree.getroot() + if level < 0: + raise ValueError(f"Initial indentation level must be >= 0, got {level}") + if not len(tree): + return + + # Reduce the memory consumption by reusing indentation strings. + indentations = ["\n" + level * space] + + def _indent_children(elem, level): + # Start a new indentation level for the first child. + child_level = level + 1 + try: + child_indentation = indentations[child_level] + except IndexError: + child_indentation = indentations[level] + space + indentations.append(child_indentation) + + if not elem.text or not elem.text.strip(): + elem.text = child_indentation + + for child in elem: + if len(child): + _indent_children(child, child_level) + if not child.tail or not child.tail.strip(): + child.tail = child_indentation + + # Dedent after the last child by overwriting the previous indentation. + if not child.tail.strip(): # type: ignore + child.tail = indentations[level] # type: ignore + + _indent_children(tree, 0) diff --git a/novelwriter/config.py b/novelwriter/config.py index 89019ce4..68f44ec4 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -196,7 +196,6 @@ class Config: # Check Python Version self.verPyString = sys.version.split()[0] - self.verPyHexVal = sys.hexversion # Check OS Type self.osType = sys.platform diff --git a/novelwriter/core/projectxml.py b/novelwriter/core/projectxml.py index 581a0ef7..a103774b 100644 --- a/novelwriter/core/projectxml.py +++ b/novelwriter/core/projectxml.py @@ -25,17 +25,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import sys import logging +import xml.etree.ElementTree as ET from enum import Enum -from lxml import etree from time import time from pathlib import Path from novelwriter import __version__, __hexversion__ from novelwriter.common import ( checkBool, checkInt, checkString, checkStringNone, formatTimeStamp, - hexToInt, simplified, yesNo + hexToInt, simplified, xmlIndent, yesNo ) from novelwriter.constants import nwFiles @@ -163,7 +164,7 @@ class ProjectXMLReader: logger.debug("Reading project XML") try: - xml = etree.parse(str(self._path)) + xml = ET.parse(str(self._path)) self._state = XMLReadState.NO_ERROR except Exception as exc: @@ -174,7 +175,7 @@ class ProjectXMLReader: backFile = self._path.with_suffix(".bak") if backFile.is_file(): try: - xml = etree.parse(str(backFile)) + xml = ET.parse(str(backFile)) self._state = XMLReadState.PARSED_BACKUP logger.info("Backup project file parsed") except Exception as exc: @@ -500,7 +501,7 @@ class ProjectXMLWriter: tStart = time() logger.debug("Writing project XML") - xRoot = etree.Element("novelWriterXML", attrib={ + xRoot = ET.Element("novelWriterXML", attrib={ "appVersion": str(__version__), "hexVersion": str(__hexversion__), "fileVersion": FILE_VERSION, @@ -515,13 +516,13 @@ class ProjectXMLWriter: "editTime": str(editTime), } - xProject = etree.SubElement(xRoot, "project", attrib=projAttr) + 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) # Save Project Settings - xSettings = etree.SubElement(xRoot, "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={ @@ -532,11 +533,11 @@ class ProjectXMLWriter: self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat) # Save Status/Importance - xStatus = etree.SubElement(xSettings, "status") + xStatus = ET.SubElement(xSettings, "status") for label, attrib in projData.itemStatus.pack(): self._packSingleValue(xStatus, "entry", label, attrib=attrib) - xImport = etree.SubElement(xSettings, "importance") + xImport = ET.SubElement(xSettings, "importance") for label, attrib in projData.itemImport.pack(): self._packSingleValue(xImport, "entry", label, attrib=attrib) @@ -547,11 +548,11 @@ class ProjectXMLWriter: "notesWords": str(projData.currCounts[1]), } - xContent = etree.SubElement(xRoot, "content", attrib=contAttr) + xContent = ET.SubElement(xRoot, "content", attrib=contAttr) for item in projContent: - xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {})) - etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {})) - xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {})) + 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", {})) xName.text = item["name"] # Write the XML tree to file @@ -559,9 +560,12 @@ class ProjectXMLWriter: tempFile = saveFile.with_suffix(".tmp") backFile = saveFile.with_suffix(".bak") try: - tempFile.write_bytes(etree.tostring( - xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True - )) + xml = ET.ElementTree(xRoot) + if sys.hexversion < 0x030900f0: + xmlIndent(xml, space=" ") + else: + ET.indent(xml, space=" ") + xml.write(tempFile, encoding="utf-8", xml_declaration=True) except Exception as exc: self._error = exc return False @@ -587,17 +591,17 @@ class ProjectXMLWriter: def _packSingleValue(self, xParent, name, value, attrib=None): """Pack a single value into an XML element. """ - xItem = etree.SubElement(xParent, name, attrib=attrib) + 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. """ - xItem = etree.SubElement(xParent, name) + xItem = ET.SubElement(xParent, name) for key, value in data.items(): if len(key) > 0: - xEntry = etree.SubElement(xItem, "entry", attrib={"key": key}) + xEntry = ET.SubElement(xItem, "entry", attrib={"key": key}) xEntry.text = str(value) or "" return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 6add5bac..89393085 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -23,6 +23,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import sys import logging from enum import Enum @@ -31,10 +32,10 @@ from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot -from PyQt5.QtGui import QIcon, QKeySequence, QCursor +from PyQt5.QtGui import QCursor, QIcon, QKeySequence from PyQt5.QtWidgets import ( - qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, - QMessageBox, QDialog, QStackedWidget + qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, + QStackedWidget, QVBoxLayout, QWidget ) from novelwriter import CONFIG, __hexversion__ @@ -88,7 +89,7 @@ class GuiMain(QMainWindow): logger.info("Host: %s", CONFIG.hostName) logger.info("Qt5: %s (0x%06x)", CONFIG.verQtString, CONFIG.verQtValue) logger.info("PyQt5: %s (0x%06x)", CONFIG.verPyQtString, CONFIG.verPyQtValue) - logger.info("Python: %s (0x%08x)", CONFIG.verPyString, CONFIG.verPyHexVal) + logger.info("Python: %s (0x%08x)", CONFIG.verPyString, sys.hexversion) logger.info("GUI Language: %s", CONFIG.guiLocale) # Core Classes diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 1e51d52f..96e22a4a 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,6 +1,6 @@ - - + + Sample Project Sample Project Jane Smith @@ -25,7 +25,7 @@ Chapter %chw%: %title% %title% Scene %ch%.%sc%: %title% - + New @@ -45,112 +45,112 @@ - + Novel - + Title Page - + Page - + Part One - + Chapter One - + Making a Scene - + Another Scene - + Interlude - + A Note on Structure - + Chapter Two - + We Found John! - + Sequel - + Title Page - + Chapter One - + Characters - + Main Characters - + John Smith - + Jane Smith - + Locations - + Earth - + Space - + Mars - + Archive - + Scenes - + Old File - + Trash - + Delete Me! - + \ No newline at end of file