diff --git a/addon/globalPlugins/readFeeds/__init__.py b/addon/globalPlugins/readFeeds/__init__.py index 10228d88..fc61a668 100644 --- a/addon/globalPlugins/readFeeds/__init__.py +++ b/addon/globalPlugins/readFeeds/__init__.py @@ -5,13 +5,12 @@ # Released under GPL 2 import os -import sys import shutil import addonHandler import globalPluginHandler import globalVars import config -import urllib +import urllib.request import scriptHandler from scriptHandler import script import api @@ -23,17 +22,14 @@ from logHandler import log import re from .skipTranslation import translate - -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) -from xml2.dom import minidom -del sys.path[-1] +from .xml.etree import ElementTree addonHandler.initTranslation() ### Constants ADDON_SUMMARY = addonHandler.getCodeAddon().manifest['summary'] -FEEDS_PATH = os.path.join(os.path.dirname(__file__), "personalFeeds").decode("mbcs") +FEEDS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "personalFeeds")) CONFIG_PATH = globalVars.appArgs.configPath DEFAULT_ADDRESS_FILE = "addressFile" # Translators: message presented when feeds cannot be reported. @@ -66,30 +62,37 @@ def doCopy(copyDirectory): shutil.copytree(FEEDS_PATH, copyDirectory) core.callLater(100, ui.message, # Translators: Message presented when feeds have been copied. - _("Feeds copied")) + _("Feeds copied") + ) except Exception as e: - wx.CallAfter(gui.messageBox, + wx.CallAfter( + gui.messageBox, # Translators: label of error dialog shown when cannot copy feeds folder. _("Folder not copied"), # Translators: title of error dialog shown when cannot copy feeds folder. _("Copy Error"), - wx.OK|wx.ICON_ERROR) + wx.OK|wx.ICON_ERROR + ) raise e def doRestore(restoreDirectory): try: shutil.rmtree(FEEDS_PATH, ignore_errors=True) shutil.copytree(restoreDirectory, FEEDS_PATH) - core.callLater(100, ui.message, + core.callLater( + 100, ui.message, # Translators: Message presented when feeds have been restored. - _("Feeds restored")) + _("Feeds restored") + ) except Exception as e: - wx.CallAfter(gui.messageBox, + wx.CallAfter( + gui.messageBox, # Translators: label of error dialog shown when cannot copy feeds folder. _("Folder not copied"), # Translators: title of error dialog shown when cannot copy feeds folder. _("Copy Error"), - wx.OK|wx.ICON_ERROR) + wx.OK|wx.ICON_ERROR + ) raise e class FeedsDialog(wx.Dialog): @@ -106,8 +109,10 @@ def __init__(self, parent): return FeedsDialog._instance = self # Translators: The title of a dialog. - super(FeedsDialog, self).__init__(parent, title=_(u"Feeds: {defaultFeed} ({configProfile})".format(configProfile=getActiveProfile(), - defaultFeed=config.conf["readFeeds"]["addressFile"]))) + super( + FeedsDialog, self).__init__(parent, title=_("Feeds: {defaultFeed} ({configProfile})".format(configProfile=getActiveProfile(), + defaultFeed=config.conf["readFeeds"]["addressFile"])) + ) mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self,orientation=wx.VERTICAL) @@ -121,8 +126,9 @@ def __init__(self, parent): changeFeedsSizer = wx.BoxSizer(wx.VERTICAL) self.choices = [os.path.splitext(filename)[0] for filename in os.listdir(FEEDS_PATH)] - self.feedsList = wx.ListBox(self, - choices=self.choices) + self.feedsList = wx.ListBox( + self, choices=self.choices + ) self.feedsList.Selection = 0 self.feedsList.Bind(wx.EVT_LISTBOX, self.onFeedsListChoice) changeFeedsSizer.Add(self.feedsList, proportion=1.0) @@ -189,17 +195,17 @@ def createFeed(self, address): feedName = api.filterFileName(feed.getFeedName()) if os.path.isfile(os.path.join(FEEDS_PATH, "%s.txt" % feedName)): feedName = "tempFeed" - with open(os.path.join(FEEDS_PATH, "%s.txt" % feedName), "w") as f: + with open(os.path.join(FEEDS_PATH, "%s.txt" % feedName), "w", encoding="utf-8") as f: f.write(address) - f.close() return feedName def onSearchEditTextChange(self, evt): self.feedsList.Clear() # Based on the filter of the Input gestures dialog of NVDA's core. filter = self.searchTextEdit.Value - filter = re.escape(filter) - filterReg = re.compile(r'(?=.*?' + r')(?=.*?'.join(filter.split('\ ')) + r')', re.U|re.IGNORECASE) + if filter: + filter = re.escape(filter) + filterReg = re.compile(r'(?=.*?' + r')(?=.*?'.join(filter.split('\ ')) + r')', re.U|re.IGNORECASE) for choice in self.choices: if filter and not filterReg.match(choice): continue @@ -208,7 +214,8 @@ def onSearchEditTextChange(self, evt): self.feedsList.Selection = 0 self.onFeedsListChoice(None) except: - [control.Disable() for control in self.feedsList, self.articlesButton, self.openButton, self.renameButton, self.deleteButton, self.defaultButton] + for control in (self.feedsList, self.articlesButton, self.openButton, self.renameButton, self.deleteButton, self.defaultButton): + control.disable def onFeedsListChoice(self, evt): self.feedsList.Enable() @@ -216,19 +223,24 @@ def onFeedsListChoice(self, evt): self.stringSel = self.feedsList.StringSelection self.articlesButton.Enabled = self.sel>= 0 self.openButton.Enabled = self.sel>= 0 - self.deleteButton.Enabled = (self.sel >= 0 and + self.deleteButton.Enabled = ( + self.sel >= 0 and self.stringSel != DEFAULT_ADDRESS_FILE and - config.conf["readFeeds"]["addressFile"] != self.stringSel) - self.renameButton.Enabled = (self.sel >= 0 and + config.conf["readFeeds"]["addressFile"] != self.stringSel + ) + self.renameButton.Enabled = ( + self.sel >= 0 and self.stringSel != DEFAULT_ADDRESS_FILE and - config.conf["readFeeds"]["addressFile"] != self.stringSel) - self.defaultButton.Enabled = (self.sel >= 0 and - self.stringSel != config.conf["readFeeds"]["addressFile"]) + config.conf["readFeeds"]["addressFile"] != self.stringSel + ) + self.defaultButton.Enabled = ( + self.sel >= 0 and + self.stringSel != config.conf["readFeeds"]["addressFile"] + ) def onArticles(self, evt): - with open(os.path.join(FEEDS_PATH, "%s.txt" % self.stringSel), "r") as f: + with open(os.path.join(FEEDS_PATH, "%s.txt" % self.stringSel), "r", encoding="utf-8") as f: address = f.read() - f.close() self.feed = Feed(address) self.Disable() try: @@ -238,16 +250,17 @@ def onArticles(self, evt): raise e def onOpen(self, evt): - with open(os.path.join(FEEDS_PATH, "%s.txt" % self.stringSel), "r") as f: + with open(os.path.join(FEEDS_PATH, "%s.txt" % self.stringSel), "r", encoding="utf-8") as f: address = f.read() - f.close() os.startfile(address) def onNew(self, evt): # Translators: The label of a field to enter an address for a new feed. - with wx.TextEntryDialog(self, _("Address of a new feed:"), + with wx.TextEntryDialog( + self, _("Address of a new feed:"), # Translators: The title of a dialog to create a new feed. - _("New feed")) as d: + _("New feed") + ) as d: if d.ShowModal() == wx.ID_CANCEL: return name = self.createFeed(d.Value) @@ -274,15 +287,19 @@ def onDefault(self, evt): def onRename(self, evt): # Translators: The label of a field to enter a new name for a feed. - with wx.TextEntryDialog(self, _("New name:"), + with wx.TextEntryDialog( + self, _("New name:"), # Translators: The title of a dialog to rename a feed. - _("Rename feed"), value=self.stringSel) as d: + _("Rename feed"), value=self.stringSel + ) as d: if d.ShowModal() == wx.ID_CANCEL or not d.Value: return curName = "%s.txt" % self.stringSel newName = "%s.txt" % api.filterFileName(d.Value) - os.rename(os.path.join(FEEDS_PATH, curName), - os.path.join(FEEDS_PATH, newName)) + os.rename( + os.path.join(FEEDS_PATH, curName), + os.path.join(FEEDS_PATH, newName) + ) self.feedsList.SetString(self.sel, os.path.splitext(newName)[0]) def onClose(self, evt): @@ -299,14 +316,14 @@ class ArticlesDialog(wx.Dialog): def __init__(self, parent): # Translators: The title of the articles dialog. - super(ArticlesDialog, self).__init__(parent, title=u"{feedTitle} ({feedNumber})".format(feedTitle=parent.stringSel, feedNumber=parent.feed.getNumberOfArticles())) + super(ArticlesDialog, self).__init__(parent, title="{feedTitle} ({feedNumber})".format(feedTitle=parent.stringSel, feedNumber=parent.feed.getNumberOfArticles())) mainSizer = wx.BoxSizer(wx.VERTICAL) sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) # Translators: The label of the articles list in the articles dialog. articlesText = _("List of articles") - articlesChoices = [parent.feed.getArticleTitle(index) for index in xrange(parent.feed.getNumberOfArticles())] + articlesChoices = [parent.feed.getArticleTitle(index) for index in range(parent.feed.getNumberOfArticles())] self.articlesList = sHelper.addLabeledControl(articlesText, wx.ListBox, choices=articlesChoices) self.articlesList.Selection = 0 self.articlesList.Bind(wx.EVT_CHOICE, self.onArticlesListChoice) @@ -338,13 +355,14 @@ def onArticlesListChoice(self, evt): os.startfile(self.Parent.feed.getArticleLink(self.articlesList.Selection)) def onArticlesListInfo(self, evt): - articleInfo = u"{title}\r\n\r\n{address}".format(title=self.Parent.feed.getArticleTitle(self.articlesList.Selection), address=self.Parent.feed.getArticleLink(self.articlesList.Selection)) + articleInfo = "{title}\r\n\r\n{address}".format(title=self.Parent.feed.getArticleTitle(self.articlesList.Selection), address=self.Parent.feed.getArticleLink(self.articlesList.Selection)) if gui.messageBox( # Translators: the label of a message box dialog. - _("%s\r\n\r\nDo you want to copy article title and link to the clipboard?" % articleInfo), + _("%sDo you want to copy article title and link to the clipboard?" % (articleInfo + "\r\n\r\n")), # Translators: the title of a message box dialog. _("Article information"), - wx.YES|wx.NO|wx.CANCEL|wx.ICON_QUESTION) == wx.YES: + wx.YES|wx.NO|wx.CANCEL|wx.ICON_QUESTION + ) == wx.YES: api.copyToClip(articleInfo) def onClose(self, evt): @@ -388,18 +406,21 @@ def __init__(self, parent): def onCopy(self, evt): if not self.copyDirectoryEdit.Value: # Message translated in NVDA core. - gui.messageBox(translate("Please specify a directory."), + gui.messageBox( + translate("Please specify a directory."), # Message translated in NVDA core. translate("Error"), wx.OK | wx.ICON_ERROR) return drv=os.path.splitdrive(self.copyDirectoryEdit.Value)[0] if drv and not os.path.isdir(drv): - # Message translated in NVDA core. - gui.messageBox(translate("Invalid drive %s")%drv, + gui.messageBox( + # Message translated in NVDA core. + translate("Invalid drive %s")%drv, # Message translated in NVDA core. translate("Error"), - wx.OK | wx.ICON_ERROR) + wx.OK | wx.ICON_ERROR + ) return self.Hide() doCopy(self.copyDirectoryEdit.Value) @@ -457,19 +478,23 @@ def __init__(self, parent): def onRestore(self, evt): if not self.restoreDirectoryEdit.Value: - # Message translated in NVDA core. - gui.messageBox(translate("Please specify a directory."), + gui.messageBox( + # Message translated in NVDA core. + translate("Please specify a directory."), # Message translated in NVDA core. translate("Error"), - wx.OK | wx.ICON_ERROR) + wx.OK | wx.ICON_ERROR + ) return drv=os.path.splitdrive(self.restoreDirectoryEdit.Value)[0] if drv and not os.path.isdir(drv): - # Message translated in NVDA core. - gui.messageBox(translate("Invalid drive %s")%drv, + gui.messageBox( + # Message translated in NVDA core. + translate("Invalid drive %s")%drv, # Message translated in NVDA core. translate("Error"), - wx.OK | wx.ICON_ERROR) + wx.OK | wx.ICON_ERROR + ) return self.Hide() doRestore(self.restoreDirectoryEdit.Value) @@ -489,24 +514,28 @@ def __init__(self, url): self._articles = [] self.refresh() + def buildTag(self, tag, ns=None): + return "%s%s" %(ns, tag) if ns else tag + def refresh(self): try: - self._document = minidom.parse(urllib.urlopen(self._url)) + self._document = ElementTree.parse(urllib.request.urlopen(self._url)) except Exception as e: raise e + tag = self._document.getroot().tag + self.ns = "%s}" % tag.split("}", 1)[0] if "}" in tag else None # Check if we are dealing with an rss or atom feed. - rssFeed = self._document.getElementsByTagName('channel') - if len(rssFeed): - self._feedType = 'rss' - self._articles = self._document.getElementsByTagName('item') + if tag.endswith("rss"): + self._main = self._document.getroot().find(self.buildTag("channel", self.ns)) + self._articles = self._main.findall(self.buildTag("item", self.ns)) + self._feedType = "rss" + elif tag.endswith("feed"): + self._main = self._document.getroot() + self._articles = self._main.findall(self.buildTag("entry", self.ns)) + self._feedType = "atom" else: - atomFeed = self._document.getElementsByTagName('feed') - if len(atomFeed): - self._feedType = 'atom' - self._articles = self._document.getElementsByTagName('entry') - else: - log.debugWarning("Unknown type of current feed", exc_info=True) - raise + log.debugWarning("Unknown type of current feed", exc_info=True) + raise self._index = 0 def getFeedUrl(self): @@ -517,14 +546,14 @@ def getFeedType(self): def getFeedName(self): try: - return self._document.getElementsByTagName('title')[0].firstChild.data + return self._main.find(self.buildTag("title", self.ns)).text except: return "" def getArticleTitle(self, index=None): if index is None: index = self._index try: - return self._articles[index].getElementsByTagName('title')[0].firstChild.data + return self._articles[index].find(self.buildTag("title", self.ns)).text except: # Translators: Presented when the current article does not have an associated title. return _("Unable to locate article title.") @@ -533,9 +562,9 @@ def getArticleLink(self, index=None): if index is None: index = self._index try: if self.getFeedType() == u'rss': - return self._articles[index].getElementsByTagName('link')[0].firstChild.data - elif self.getFeedType() == u'atom': - return self._articles[index].getElementsByTagName('link')[0].getAttribute('href') + return self._articles[index].find(self.buildTag("link", self.ns)).text + elif self.getFeedType() == 'atom': + return self._articles[index].find(self.buildTag("link", self.ns)).get("href") except: # Translators: Presented when the current article does not have an associated link. return _("Unable to locate article link.") @@ -557,7 +586,7 @@ def getNumberOfArticles(self): class GlobalPlugin(globalPluginHandler.GlobalPlugin): - scriptCategory = unicode(ADDON_SUMMARY) + scriptCategory = ADDON_SUMMARY def __init__(self): super(GlobalPlugin, self).__init__() @@ -625,9 +654,8 @@ def script_activateRestoreDialog(self, gesture): def getFirstArticle(self): addressFile = "%s.txt" % config.conf["readFeeds"]["addressFile"] - with open(os.path.join(FEEDS_PATH, addressFile), "r") as f: + with open(os.path.join(FEEDS_PATH, addressFile), "r", encoding="utf-8") as f: address = f.read() - f.close() if self.feed and self.feed.getFeedUrl() == address: curFeed = self.feed else: @@ -657,7 +685,7 @@ def script_readFirstArticle(self, gesture): def script_readCurrentArticle(self, gesture): if not self.feed: self.getFirstArticle() - articleInfo = u"{title}\r\n\r\n{address}".format(title=self.feed.getArticleTitle(), address=self.feed.getArticleLink()) + articleInfo = "{title}\r\n\r\n{address}".format(title=self.feed.getArticleTitle(), address=self.feed.getArticleLink()) if scriptHandler.getLastScriptRepeatCount()==1 and api.copyToClip(articleInfo): # Translators: message presented when the information about an article of a feed is copied to the clipboard. ui.message(_("Copied to clipboard %s") % articleInfo) @@ -707,7 +735,7 @@ def script_reportLink(self, gesture): def script_copyArticleInfo(self, gesture): if not self.feed: self.getFirstArticle() - articleInfo = u"{title}\r\n\r\n{address}".format(title=self.feed.getArticleTitle(), address=self.feed.getArticleLink()) + articleInfo = "{title}\r\n\r\n{address}".format(title=self.feed.getArticleTitle(), address=self.feed.getArticleLink()) if api.copyToClip(articleInfo): # Translators: message presented when the information about an article of a feed is copied to the clipboard. ui.message(_("Copied to clipboard %s") % articleInfo) diff --git a/addon/xml2/__init__.py b/addon/globalPlugins/readFeeds/xml/__init__.py similarity index 55% rename from addon/xml2/__init__.py rename to addon/globalPlugins/readFeeds/xml/__init__.py index 65871b91..bf6d8ddf 100644 --- a/addon/xml2/__init__.py +++ b/addon/globalPlugins/readFeeds/xml/__init__.py @@ -18,27 +18,3 @@ __all__ = ["dom", "parsers", "sax", "etree"] - -_MINIMUM_XMLPLUS_VERSION = (0, 8, 4) - - -try: - import _xmlplus -except ImportError: - pass -else: - try: - v = _xmlplus.version_info - except AttributeError: - # _xmlplus is too old; ignore it - pass - else: - if v >= _MINIMUM_XMLPLUS_VERSION: - import sys - _xmlplus.__path__.extend(__path__) - sys.modules[__name__] = _xmlplus - else: - del v - -import globalPluginHandler - diff --git a/addon/xml2/dom/NodeFilter.py b/addon/globalPlugins/readFeeds/xml/dom/NodeFilter.py similarity index 95% rename from addon/xml2/dom/NodeFilter.py rename to addon/globalPlugins/readFeeds/xml/dom/NodeFilter.py index fc052459..640e0bfd 100644 --- a/addon/xml2/dom/NodeFilter.py +++ b/addon/globalPlugins/readFeeds/xml/dom/NodeFilter.py @@ -9,7 +9,7 @@ class NodeFilter: FILTER_REJECT = 2 FILTER_SKIP = 3 - SHOW_ALL = 0xFFFFFFFFL + SHOW_ALL = 0xFFFFFFFF SHOW_ELEMENT = 0x00000001 SHOW_ATTRIBUTE = 0x00000002 SHOW_TEXT = 0x00000004 diff --git a/addon/xml2/dom/__init__.py b/addon/globalPlugins/readFeeds/xml/dom/__init__.py similarity index 97% rename from addon/xml2/dom/__init__.py rename to addon/globalPlugins/readFeeds/xml/dom/__init__.py index 6363d006..97cf9a64 100644 --- a/addon/xml2/dom/__init__.py +++ b/addon/globalPlugins/readFeeds/xml/dom/__init__.py @@ -17,6 +17,7 @@ class Node: """Class giving the NodeType constants.""" + __slots__ = () # DOM implementations may use this as a base class for their own # Node implementations. If they don't, the constants defined here @@ -136,4 +137,4 @@ class UserDataHandler: EMPTY_NAMESPACE = None EMPTY_PREFIX = None -from domreg import getDOMImplementation,registerDOMImplementation +from .domreg import getDOMImplementation, registerDOMImplementation diff --git a/addon/xml2/dom/domreg.py b/addon/globalPlugins/readFeeds/xml/dom/domreg.py similarity index 88% rename from addon/xml2/dom/domreg.py rename to addon/globalPlugins/readFeeds/xml/dom/domreg.py index ea8b7c93..69c17eeb 100644 --- a/addon/xml2/dom/domreg.py +++ b/addon/globalPlugins/readFeeds/xml/dom/domreg.py @@ -2,12 +2,12 @@ directly. Instead, the functions getDOMImplementation and registerDOMImplementation should be imported from xml.dom.""" -from xml2.dom.minicompat import * # isinstance, StringTypes - # This is a list of well-known implementations. Well-known names # should be published by posting to xml-sig@python.org, and are # subsequently recorded in this file. +import sys + well_known_implementations = { 'minidom':'xml.dom.minidom', '4DOM': 'xml.dom.DOMImplementation', @@ -36,7 +36,7 @@ def _good_enough(dom, features): return 0 return 1 -def getDOMImplementation(name = None, features = ()): +def getDOMImplementation(name=None, features=()): """getDOMImplementation(name = None, features = ()) -> DOM implementation. Return a suitable DOM implementation. The name is either @@ -57,12 +57,12 @@ def getDOMImplementation(name = None, features = ()): return mod.getDOMImplementation() elif name: return registered[name]() - elif "PYTHON_DOM" in os.environ: + elif not sys.flags.ignore_environment and "PYTHON_DOM" in os.environ: return getDOMImplementation(name = os.environ["PYTHON_DOM"]) # User did not specify a name, try implementations in arbitrary # order, returning the one that has the required features - if isinstance(features, StringTypes): + if isinstance(features, str): features = _parse_feature_string(features) for creator in registered.values(): dom = creator() @@ -72,12 +72,12 @@ def getDOMImplementation(name = None, features = ()): for creator in well_known_implementations.keys(): try: dom = getDOMImplementation(name = creator) - except StandardError: # typically ImportError, or AttributeError + except Exception: # typically ImportError, or AttributeError continue if _good_enough(dom, features): return dom - raise ImportError,"no suitable DOM implementation found" + raise ImportError("no suitable DOM implementation found") def _parse_feature_string(s): features = [] @@ -87,7 +87,7 @@ def _parse_feature_string(s): while i < length: feature = parts[i] if feature[0] in "0123456789": - raise ValueError, "bad feature name: %r" % (feature,) + raise ValueError("bad feature name: %r" % (feature,)) i = i + 1 version = None if i < length: diff --git a/addon/xml2/dom/expatbuilder.py b/addon/globalPlugins/readFeeds/xml/dom/expatbuilder.py similarity index 94% rename from addon/xml2/dom/expatbuilder.py rename to addon/globalPlugins/readFeeds/xml/dom/expatbuilder.py index ddfef119..2bd835b0 100644 --- a/addon/xml2/dom/expatbuilder.py +++ b/addon/globalPlugins/readFeeds/xml/dom/expatbuilder.py @@ -10,7 +10,7 @@ # minidom DOM and can't be used with other DOM implementations. This # is due, in part, to a lack of appropriate methods in the DOM (there is # no way to create Entity and Notation nodes via the DOM Level 2 -# interface), and for performance. The later is the cause of some fairly +# interface), and for performance. The latter is the cause of some fairly # cryptic code. # # Performance hacks: @@ -27,13 +27,11 @@ # calling any methods on the node object if it exists. (A rather # nice speedup is achieved this way as well!) -from xml2.dom import xmlbuilder, minidom, Node -from xml2.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE -from xml2.parsers import expat -from xml2.dom.minidom import _append_child, _set_attribute_node -from xml2.dom.NodeFilter import NodeFilter - -from xml2.dom.minicompat import * +from xml.dom import xmlbuilder, minidom, Node +from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE +from xml.parsers import expat +from xml.dom.minidom import _append_child, _set_attribute_node +from xml.dom.NodeFilter import NodeFilter TEXT_NODE = Node.TEXT_NODE CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE @@ -123,10 +121,12 @@ def _parse_ns_name(builder, name): qname = "%s:%s" % (prefix, localname) qname = intern(qname, qname) localname = intern(localname, localname) - else: + elif len(parts) == 2: uri, localname = parts prefix = EMPTY_PREFIX qname = localname = intern(localname, localname) + else: + raise ValueError("Unsupported syntax: spaces in URIs not supported: %r" % name) return intern(uri, uri), localname, prefix, qname @@ -283,27 +283,23 @@ def character_data_handler_cdata(self, data): elif childNodes and childNodes[-1].nodeType == TEXT_NODE: node = childNodes[-1] value = node.data + data - d = node.__dict__ - d['data'] = d['nodeValue'] = value + node.data = value return else: node = minidom.Text() - d = node.__dict__ - d['data'] = d['nodeValue'] = data - d['ownerDocument'] = self.document + node.data = data + node.ownerDocument = self.document _append_child(self.curNode, node) def character_data_handler(self, data): childNodes = self.curNode.childNodes if childNodes and childNodes[-1].nodeType == TEXT_NODE: node = childNodes[-1] - d = node.__dict__ - d['data'] = d['nodeValue'] = node.data + data + node.data = node.data + data return node = minidom.Text() - d = node.__dict__ - d['data'] = d['nodeValue'] = node.data + data - d['ownerDocument'] = self.document + node.data = node.data + data + node.ownerDocument = self.document _append_child(self.curNode, node) def entity_decl_handler(self, entityName, is_parameter_entity, value, @@ -363,11 +359,8 @@ def start_element_handler(self, name, attributes): a = minidom.Attr(attributes[i], EMPTY_NAMESPACE, None, EMPTY_PREFIX) value = attributes[i+1] - d = a.childNodes[0].__dict__ - d['data'] = d['nodeValue'] = value - d = a.__dict__ - d['value'] = d['nodeValue'] = value - d['ownerDocument'] = self.document + a.value = value + a.ownerDocument = self.document _set_attribute_node(node, a) if node is not self.document.documentElement: @@ -476,8 +469,8 @@ def startContainer(self, node): if val == FILTER_INTERRUPT: raise ParseEscape if val not in _ALLOWED_FILTER_RETURNS: - raise ValueError, \ - "startContainer() returned illegal value: " + repr(val) + raise ValueError( + "startContainer() returned illegal value: " + repr(val)) return val else: return FILTER_ACCEPT @@ -496,8 +489,8 @@ def acceptNode(self, node): # node is handled by the caller return FILTER_REJECT if val not in _ALLOWED_FILTER_RETURNS: - raise ValueError, \ - "acceptNode() returned illegal value: " + repr(val) + raise ValueError( + "acceptNode() returned illegal value: " + repr(val)) return val else: return FILTER_ACCEPT @@ -761,15 +754,13 @@ def start_element_handler(self, name, attributes): else: a = minidom.Attr("xmlns", XMLNS_NAMESPACE, "xmlns", EMPTY_PREFIX) - d = a.childNodes[0].__dict__ - d['data'] = d['nodeValue'] = uri - d = a.__dict__ - d['value'] = d['nodeValue'] = uri - d['ownerDocument'] = self.document + a.value = uri + a.ownerDocument = self.document _set_attribute_node(node, a) del self._ns_ordered_prefixes[:] if attributes: + node._ensure_attributes() _attrs = node._attrs _attrsNS = node._attrsNS for i in range(0, len(attributes), 2): @@ -785,12 +776,9 @@ def start_element_handler(self, name, attributes): aname, EMPTY_PREFIX) _attrs[aname] = a _attrsNS[(EMPTY_NAMESPACE, aname)] = a - d = a.childNodes[0].__dict__ - d['data'] = d['nodeValue'] = value - d = a.__dict__ - d['ownerDocument'] = self.document - d['value'] = d['nodeValue'] = value - d['ownerElement'] = node + a.ownerDocument = self.document + a.value = value + a.ownerElement = node if __debug__: # This only adds some asserts to the original @@ -918,12 +906,9 @@ def parse(file, namespaces=True): else: builder = ExpatBuilder() - if isinstance(file, StringTypes): - fp = open(file, 'rb') - try: + if isinstance(file, str): + with open(file, 'rb') as fp: result = builder.parseFile(fp) - finally: - fp.close() else: result = builder.parseFile(file) return result @@ -952,12 +937,9 @@ def parseFragment(file, context, namespaces=True): else: builder = FragmentBuilder(context) - if isinstance(file, StringTypes): - fp = open(file, 'rb') - try: + if isinstance(file, str): + with open(file, 'rb') as fp: result = builder.parseFile(fp) - finally: - fp.close() else: result = builder.parseFile(file) return result diff --git a/addon/xml2/dom/minicompat.py b/addon/globalPlugins/readFeeds/xml/dom/minicompat.py similarity index 90% rename from addon/xml2/dom/minicompat.py rename to addon/globalPlugins/readFeeds/xml/dom/minicompat.py index 27299f4e..5d6fae9a 100644 --- a/addon/xml2/dom/minicompat.py +++ b/addon/globalPlugins/readFeeds/xml/dom/minicompat.py @@ -1,4 +1,8 @@ -"""Python version compatibility support for minidom.""" +"""Python version compatibility support for minidom. + +This module contains internal implementation details and +should not be imported; use xml.dom.minidom instead. +""" # This module should only be imported using "import *". # @@ -38,14 +42,9 @@ __all__ = ["NodeList", "EmptyNodeList", "StringTypes", "defproperty"] -import xml2.dom +import xml.dom -try: - unicode -except NameError: - StringTypes = type(''), -else: - StringTypes = type(''), type(unicode('')) +StringTypes = (str,) class NodeList(list): @@ -65,10 +64,10 @@ def _set_length(self, value): length = property(_get_length, _set_length, doc="The number of nodes in the NodeList.") - def __getstate__(self): - return list(self) - + # For backward compatibility def __setstate__(self, state): + if state is None: + state = [] self[:] = state @@ -100,7 +99,7 @@ def _set_length(self, value): def defproperty(klass, name, doc): - get = getattr(klass, ("_get_" + name)).im_func + get = getattr(klass, ("_get_" + name)) def set(self, value, name=name): raise xml.dom.NoModificationAllowedErr( "attempt to modify read-only attribute " + repr(name)) diff --git a/addon/xml2/dom/minidom.py b/addon/globalPlugins/readFeeds/xml/dom/minidom.py similarity index 83% rename from addon/xml2/dom/minidom.py rename to addon/globalPlugins/readFeeds/xml/dom/minidom.py index 74f552bc..24957ea1 100644 --- a/addon/xml2/dom/minidom.py +++ b/addon/globalPlugins/readFeeds/xml/dom/minidom.py @@ -1,5 +1,6 @@ -"""\ -minidom.py -- a lightweight DOM implementation. +"""Simple implementation of the Level 1 DOM. + +Namespaces and other minor Level 2 features are also supported. parse("foo.xml") @@ -14,22 +15,23 @@ * SAX 2 namespaces """ -import xml2.dom +import io +import xml.dom -from xml2.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg -from xml2.dom.minicompat import * -from xml2.dom.xmlbuilder import DOMImplementationLS, DocumentLS +from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg +from xml.dom.minicompat import * +from xml.dom.xmlbuilder import DOMImplementationLS, DocumentLS # This is used by the ID-cache invalidation checks; the list isn't # actually complete, since the nodes being checked will never be the # DOCUMENT_NODE or DOCUMENT_FRAGMENT_NODE. (The node being checked is # the node being added or removed, not the node being modified.) # -_nodeTypes_with_children = (xml2.dom.Node.ELEMENT_NODE, - xml2.dom.Node.ENTITY_REFERENCE_NODE) +_nodeTypes_with_children = (xml.dom.Node.ELEMENT_NODE, + xml.dom.Node.ENTITY_REFERENCE_NODE) -class Node(xml2.dom.Node): +class Node(xml.dom.Node): namespaceURI = None # this is non-null only for elements and attributes parentNode = None ownerDocument = None @@ -38,32 +40,32 @@ class Node(xml2.dom.Node): prefix = EMPTY_PREFIX # non-null only for NS elements and attributes - def __nonzero__(self): + def __bool__(self): return True - def toxml(self, encoding = None): + def toxml(self, encoding=None): return self.toprettyxml("", "", encoding) - def toprettyxml(self, indent="\t", newl="\n", encoding = None): - # indent = the indentation string to prepend, per level - # newl = the newline string to append - writer = _get_StringIO() - if encoding is not None: - import codecs - # Can't use codecs.getwriter to preserve 2.0 compatibility - writer = codecs.lookup(encoding)[3](writer) + def toprettyxml(self, indent="\t", newl="\n", encoding=None): + if encoding is None: + writer = io.StringIO() + else: + writer = io.TextIOWrapper(io.BytesIO(), + encoding=encoding, + errors="xmlcharrefreplace", + newline='\n') if self.nodeType == Node.DOCUMENT_NODE: # Can pass encoding only to document, to put it into XML header self.writexml(writer, "", indent, newl, encoding) else: self.writexml(writer, "", indent, newl) - return writer.getvalue() + if encoding is None: + return writer.getvalue() + else: + return writer.detach().getvalue() def hasChildNodes(self): - if self.childNodes: - return True - else: - return False + return bool(self.childNodes) def _get_childNodes(self): return self.childNodes @@ -83,7 +85,7 @@ def insertBefore(self, newChild, refChild): ### The DOM does not clearly specify what to return in this case return newChild if newChild.nodeType not in self._child_node_types: - raise xml2dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "%s cannot be child of %s" % (repr(newChild), repr(self))) if newChild.parentNode is not None: newChild.parentNode.removeChild(newChild) @@ -93,7 +95,7 @@ def insertBefore(self, newChild, refChild): try: index = self.childNodes.index(refChild) except ValueError: - raise xml2dom.NotFoundErr() + raise xml.dom.NotFoundErr() if newChild.nodeType in _nodeTypes_with_children: _clear_id_cache(self) self.childNodes.insert(index, newChild) @@ -115,7 +117,7 @@ def appendChild(self, node): ### The DOM does not clearly specify what to return in this case return node if node.nodeType not in self._child_node_types: - raise xml2dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "%s cannot be child of %s" % (repr(node), repr(self))) elif node.nodeType in _nodeTypes_with_children: _clear_id_cache(self) @@ -131,7 +133,7 @@ def replaceChild(self, newChild, oldChild): self.removeChild(oldChild) return self.insertBefore(newChild, refChild) if newChild.nodeType not in self._child_node_types: - raise xml2dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "%s cannot be child of %s" % (repr(newChild), repr(self))) if newChild is oldChild: return @@ -140,7 +142,7 @@ def replaceChild(self, newChild, oldChild): try: index = self.childNodes.index(oldChild) except ValueError: - raise xml2dom.NotFoundErr() + raise xml.dom.NotFoundErr() self.childNodes[index] = newChild newChild.parentNode = self oldChild.parentNode = None @@ -161,7 +163,7 @@ def removeChild(self, oldChild): try: self.childNodes.remove(oldChild) except ValueError: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() if oldChild.nextSibling is not None: oldChild.nextSibling.previousSibling = oldChild.previousSibling if oldChild.previousSibling is not None: @@ -251,7 +253,7 @@ def setUserData(self, key, data, handler): def _call_user_data_handler(self, operation, src, dst): if hasattr(self, "_user_data"): - for key, (data, handler) in self._user_data.items(): + for key, (data, handler) in list(self._user_data.items()): if handler is not None: handler.handle(operation, key, data, src, dst) @@ -266,6 +268,14 @@ def unlink(self): self.previousSibling = None self.nextSibling = None + # A Node is its own context manager, to ensure that an unlink() call occurs. + # This is similar to how a file object works. + def __enter__(self): + return self + + def __exit__(self, et, ev, tb): + self.unlink() + defproperty(Node, "firstChild", doc="First child node, or None.") defproperty(Node, "lastChild", doc="Last child node, or None.") defproperty(Node, "localName", doc="Namespace-local name of this node.") @@ -276,10 +286,10 @@ def _append_child(self, node): childNodes = self.childNodes if childNodes: last = childNodes[-1] - node.__dict__["previousSibling"] = last - last.__dict__["nextSibling"] = node + node.previousSibling = last + last.nextSibling = node childNodes.append(node) - node.__dict__["parentNode"] = self + node.parentNode = self def _in_document(node): # return True iff node is part of a document tree @@ -332,9 +342,10 @@ def __init__(self): class Attr(Node): + __slots__=('_name', '_value', 'namespaceURI', + '_prefix', 'childNodes', '_localName', 'ownerDocument', 'ownerElement') nodeType = Node.ATTRIBUTE_NODE attributes = None - ownerElement = None specified = False _is_id = False @@ -342,12 +353,11 @@ class Attr(Node): def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None, prefix=None): - # skip setattr for performance - d = self.__dict__ - d["nodeName"] = d["name"] = qName - d["namespaceURI"] = namespaceURI - d["prefix"] = prefix - d['childNodes'] = NodeList() + self.ownerElement = None + self._name = qName + self.namespaceURI = namespaceURI + self._prefix = prefix + self.childNodes = NodeList() # Add the single child node that represents the value of the attr self.childNodes.append(Text()) @@ -355,51 +365,55 @@ def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None, # nodeValue and value are set elsewhere def _get_localName(self): - return self.nodeName.split(":", 1)[-1] - - def _get_name(self): - return self.name + try: + return self._localName + except AttributeError: + return self.nodeName.split(":", 1)[-1] def _get_specified(self): return self.specified - def __setattr__(self, name, value): - d = self.__dict__ - if name in ("value", "nodeValue"): - d["value"] = d["nodeValue"] = value - d2 = self.childNodes[0].__dict__ - d2["data"] = d2["nodeValue"] = value - if self.ownerElement is not None: - _clear_id_cache(self.ownerElement) - elif name in ("name", "nodeName"): - d["name"] = d["nodeName"] = value - if self.ownerElement is not None: - _clear_id_cache(self.ownerElement) - else: - d[name] = value + def _get_name(self): + return self._name + + def _set_name(self, value): + self._name = value + if self.ownerElement is not None: + _clear_id_cache(self.ownerElement) + + nodeName = name = property(_get_name, _set_name) + + def _get_value(self): + return self._value + + def _set_value(self, value): + self._value = value + self.childNodes[0].data = value + if self.ownerElement is not None: + _clear_id_cache(self.ownerElement) + self.childNodes[0].data = value + + nodeValue = value = property(_get_value, _set_value) + + def _get_prefix(self): + return self._prefix def _set_prefix(self, prefix): nsuri = self.namespaceURI if prefix == "xmlns": if nsuri and nsuri != XMLNS_NAMESPACE: - raise xml2.dom.NamespaceErr( + raise xml.dom.NamespaceErr( "illegal use of 'xmlns' prefix for the wrong namespace") - d = self.__dict__ - d['prefix'] = prefix + self._prefix = prefix if prefix is None: newName = self.localName else: newName = "%s:%s" % (prefix, self.localName) if self.ownerElement: _clear_id_cache(self.ownerElement) - d['nodeName'] = d['name'] = newName + self.name = newName - def _set_value(self, value): - d = self.__dict__ - d['value'] = d['nodeValue'] = value - if self.ownerElement: - _clear_id_cache(self.ownerElement) - self.childNodes[0].data = value + prefix = property(_get_prefix, _set_prefix) def unlink(self): # This implementation does not call the base implementation @@ -474,7 +488,7 @@ def _get_length(self): def item(self, index): try: - return self[self._attrs.keys()[index]] + return self[list(self._attrs.keys())[index]] except IndexError: return None @@ -490,8 +504,8 @@ def itemsNS(self): L.append(((node.namespaceURI, node.localName), node.value)) return L - def has_key(self, key): - if isinstance(key, StringTypes): + def __contains__(self, key): + if isinstance(key, str): return key in self._attrs else: return key in self._attrsNS @@ -510,12 +524,26 @@ def get(self, name, value=None): __len__ = _get_length - __hash__ = None # Mutable type can't be correctly hashed - def __cmp__(self, other): + def _cmp(self, other): if self._attrs is getattr(other, "_attrs", None): return 0 else: - return cmp(id(self), id(other)) + return (id(self) > id(other)) - (id(self) < id(other)) + + def __eq__(self, other): + return self._cmp(other) == 0 + + def __ge__(self, other): + return self._cmp(other) >= 0 + + def __gt__(self, other): + return self._cmp(other) > 0 + + def __le__(self, other): + return self._cmp(other) <= 0 + + def __lt__(self, other): + return self._cmp(other) < 0 def __getitem__(self, attname_or_tuple): if isinstance(attname_or_tuple, tuple): @@ -525,7 +553,7 @@ def __getitem__(self, attname_or_tuple): # same as set def __setitem__(self, attname, value): - if isinstance(value, StringTypes): + if isinstance(value, str): try: node = self._attrs[attname] except KeyError: @@ -535,7 +563,7 @@ def __setitem__(self, attname, value): node.value = value else: if not isinstance(value, Attr): - raise TypeError, "value must be a string or Attr object" + raise TypeError("value must be a string or Attr object") node = value self.setNamedItem(node) @@ -557,11 +585,11 @@ def removeNamedItem(self, name): _clear_id_cache(self._ownerElement) del self._attrs[n.nodeName] del self._attrsNS[(n.namespaceURI, n.localName)] - if 'ownerElement' in n.__dict__: - n.__dict__['ownerElement'] = None + if hasattr(n, 'ownerElement'): + n.ownerElement = None return n else: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() def removeNamedItemNS(self, namespaceURI, localName): n = self.getNamedItemNS(namespaceURI, localName) @@ -569,15 +597,15 @@ def removeNamedItemNS(self, namespaceURI, localName): _clear_id_cache(self._ownerElement) del self._attrsNS[(n.namespaceURI, n.localName)] del self._attrs[n.nodeName] - if 'ownerElement' in n.__dict__: - n.__dict__['ownerElement'] = None + if hasattr(n, 'ownerElement'): + n.ownerElement = None return n else: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() def setNamedItem(self, node): if not isinstance(node, Attr): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "%s cannot be child of %s" % (repr(node), repr(self))) old = self._attrs.get(node.name) if old: @@ -617,9 +645,10 @@ def __init__(self, namespace, name): def __repr__(self): if self.namespace: - return "" % (self.name, self.namespace) + return "<%s %r (from %r)>" % (self.__class__.__name__, self.name, + self.namespace) else: - return "" % self.name + return "<%s %r>" % (self.__class__.__name__, self.name) def _get_name(self): return self.name @@ -630,6 +659,9 @@ def _get_namespace(self): _no_type = TypeInfo(None, None) class Element(Node): + __slots__=('ownerDocument', 'parentNode', 'tagName', 'nodeName', 'prefix', + 'namespaceURI', '_localName', 'childNodes', '_attrs', '_attrsNS', + 'nextSibling', 'previousSibling') nodeType = Node.ELEMENT_NODE nodeValue = None schemaType = _no_type @@ -645,39 +677,57 @@ class Element(Node): def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None, localName=None): + self.parentNode = None self.tagName = self.nodeName = tagName self.prefix = prefix self.namespaceURI = namespaceURI self.childNodes = NodeList() + self.nextSibling = self.previousSibling = None + + # Attribute dictionaries are lazily created + # attributes are double-indexed: + # tagName -> Attribute + # URI,localName -> Attribute + # in the future: consider lazy generation + # of attribute objects this is too tricky + # for now because of headaches with + # namespaces. + self._attrs = None + self._attrsNS = None - self._attrs = {} # attributes are double-indexed: - self._attrsNS = {} # tagName -> Attribute - # URI,localName -> Attribute - # in the future: consider lazy generation - # of attribute objects this is too tricky - # for now because of headaches with - # namespaces. + def _ensure_attributes(self): + if self._attrs is None: + self._attrs = {} + self._attrsNS = {} def _get_localName(self): - return self.tagName.split(":", 1)[-1] + try: + return self._localName + except AttributeError: + return self.tagName.split(":", 1)[-1] def _get_tagName(self): return self.tagName def unlink(self): - for attr in self._attrs.values(): - attr.unlink() + if self._attrs is not None: + for attr in list(self._attrs.values()): + attr.unlink() self._attrs = None self._attrsNS = None Node.unlink(self) def getAttribute(self, attname): + if self._attrs is None: + return "" try: return self._attrs[attname].value except KeyError: return "" def getAttributeNS(self, namespaceURI, localName): + if self._attrsNS is None: + return "" try: return self._attrsNS[(namespaceURI, localName)].value except KeyError: @@ -687,14 +737,11 @@ def setAttribute(self, attname, value): attr = self.getAttributeNode(attname) if attr is None: attr = Attr(attname) - # for performance - d = attr.__dict__ - d["value"] = d["nodeValue"] = value - d["ownerDocument"] = self.ownerDocument + attr.value = value # also sets nodeValue + attr.ownerDocument = self.ownerDocument self.setAttributeNode(attr) elif value != attr.value: - d = attr.__dict__ - d["value"] = d["nodeValue"] = value + attr.value = value if attr.isId: _clear_id_cache(self) @@ -702,33 +749,33 @@ def setAttributeNS(self, namespaceURI, qualifiedName, value): prefix, localname = _nssplit(qualifiedName) attr = self.getAttributeNodeNS(namespaceURI, localname) if attr is None: - # for performance attr = Attr(qualifiedName, namespaceURI, localname, prefix) - d = attr.__dict__ - d["prefix"] = prefix - d["nodeName"] = qualifiedName - d["value"] = d["nodeValue"] = value - d["ownerDocument"] = self.ownerDocument + attr.value = value + attr.ownerDocument = self.ownerDocument self.setAttributeNode(attr) else: - d = attr.__dict__ if value != attr.value: - d["value"] = d["nodeValue"] = value + attr.value = value if attr.isId: _clear_id_cache(self) if attr.prefix != prefix: - d["prefix"] = prefix - d["nodeName"] = qualifiedName + attr.prefix = prefix + attr.nodeName = qualifiedName def getAttributeNode(self, attrname): + if self._attrs is None: + return None return self._attrs.get(attrname) def getAttributeNodeNS(self, namespaceURI, localName): + if self._attrsNS is None: + return None return self._attrsNS.get((namespaceURI, localName)) def setAttributeNode(self, attr): if attr.ownerElement not in (None, self): - raise xml2.dom.InuseAttributeErr("attribute node already owned") + raise xml.dom.InuseAttributeErr("attribute node already owned") + self._ensure_attributes() old1 = self._attrs.get(attr.name, None) if old1 is not None: self.removeAttributeNode(old1) @@ -747,26 +794,30 @@ def setAttributeNode(self, attr): setAttributeNodeNS = setAttributeNode def removeAttribute(self, name): + if self._attrsNS is None: + raise xml.dom.NotFoundErr() try: attr = self._attrs[name] except KeyError: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() self.removeAttributeNode(attr) def removeAttributeNS(self, namespaceURI, localName): + if self._attrsNS is None: + raise xml.dom.NotFoundErr() try: attr = self._attrsNS[(namespaceURI, localName)] except KeyError: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() self.removeAttributeNode(attr) def removeAttributeNode(self, node): if node is None: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() try: self._attrs[node.name] except KeyError: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() _clear_id_cache(self) node.unlink() # Restore this since the node is still useful and otherwise @@ -776,9 +827,13 @@ def removeAttributeNode(self, node): removeAttributeNodeNS = removeAttributeNode def hasAttribute(self, name): + if self._attrs is None: + return False return name in self._attrs def hasAttributeNS(self, namespaceURI, localName): + if self._attrsNS is None: + return False return (namespaceURI, localName) in self._attrsNS def getElementsByTagName(self, name): @@ -798,22 +853,28 @@ def writexml(self, writer, indent="", addindent="", newl=""): writer.write(indent+"<" + self.tagName) attrs = self._get_attributes() - a_names = attrs.keys() - a_names.sort() + a_names = sorted(attrs.keys()) for a_name in a_names: writer.write(" %s=\"" % a_name) _write_data(writer, attrs[a_name].value) writer.write("\"") if self.childNodes: - writer.write(">%s"%(newl)) - for node in self.childNodes: - node.writexml(writer,indent+addindent,addindent,newl) - writer.write("%s%s" % (indent,self.tagName,newl)) + writer.write(">") + if (len(self.childNodes) == 1 and + self.childNodes[0].nodeType == Node.TEXT_NODE): + self.childNodes[0].writexml(writer, '', '', '') + else: + writer.write(newl) + for node in self.childNodes: + node.writexml(writer, indent+addindent, addindent, newl) + writer.write(indent) + writer.write("%s" % (self.tagName, newl)) else: writer.write("/>%s"%(newl)) def _get_attributes(self): + self._ensure_attributes() return NamedNodeMap(self._attrs, self._attrsNS, self) def hasAttributes(self): @@ -834,11 +895,11 @@ def setIdAttributeNS(self, namespaceURI, localName): def setIdAttributeNode(self, idAttr): if idAttr is None or not self.isSameNode(idAttr.ownerElement): - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() if _get_containing_entref(self) is not None: - raise xml2.dom.NoModificationAllowedErr() + raise xml.dom.NoModificationAllowedErr() if not idAttr._is_id: - idAttr.__dict__['_is_id'] = True + idAttr._is_id = True self._magic_id_nodes += 1 self.ownerDocument._magic_id_count += 1 _clear_id_cache(self) @@ -851,19 +912,20 @@ def setIdAttributeNode(self, idAttr): def _set_attribute_node(element, attr): _clear_id_cache(element) + element._ensure_attributes() element._attrs[attr.name] = attr element._attrsNS[(attr.namespaceURI, attr.localName)] = attr # This creates a circular reference, but Element.unlink() # breaks the cycle since the references to the attribute # dictionaries are tossed. - attr.__dict__['ownerElement'] = element - + attr.ownerElement = element class Childless: """Mixin that makes childless-ness easy to implement and avoids the complexity of the Node methods that deal with children. """ + __slots__ = () attributes = None childNodes = EmptyNodeList() @@ -877,18 +939,18 @@ def _get_lastChild(self): return None def appendChild(self, node): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( self.nodeName + " nodes cannot have children") def hasChildNodes(self): return False def insertBefore(self, newChild, refChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( self.nodeName + " nodes do not have children") def removeChild(self, oldChild): - raise xml2.dom.NotFoundErr( + raise xml.dom.NotFoundErr( self.nodeName + " nodes do not have children") def normalize(self): @@ -896,60 +958,55 @@ def normalize(self): pass def replaceChild(self, newChild, oldChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( self.nodeName + " nodes do not have children") class ProcessingInstruction(Childless, Node): nodeType = Node.PROCESSING_INSTRUCTION_NODE + __slots__ = ('target', 'data') def __init__(self, target, data): - self.target = self.nodeName = target - self.data = self.nodeValue = data + self.target = target + self.data = data - def _get_data(self): + # nodeValue is an alias for data + def _get_nodeValue(self): return self.data - def _set_data(self, value): - d = self.__dict__ - d['data'] = d['nodeValue'] = value + def _set_nodeValue(self, value): + self.data = value + nodeValue = property(_get_nodeValue, _set_nodeValue) - def _get_target(self): + # nodeName is an alias for target + def _get_nodeName(self): return self.target - def _set_target(self, value): - d = self.__dict__ - d['target'] = d['nodeName'] = value - - def __setattr__(self, name, value): - if name == "data" or name == "nodeValue": - self.__dict__['data'] = self.__dict__['nodeValue'] = value - elif name == "target" or name == "nodeName": - self.__dict__['target'] = self.__dict__['nodeName'] = value - else: - self.__dict__[name] = value + def _set_nodeName(self, value): + self.target = value + nodeName = property(_get_nodeName, _set_nodeName) def writexml(self, writer, indent="", addindent="", newl=""): writer.write("%s%s" % (indent,self.target, self.data, newl)) class CharacterData(Childless, Node): + __slots__=('_data', 'ownerDocument','parentNode', 'previousSibling', 'nextSibling') + + def __init__(self): + self.ownerDocument = self.parentNode = None + self.previousSibling = self.nextSibling = None + self._data = '' + Node.__init__(self) + def _get_length(self): return len(self.data) __len__ = _get_length def _get_data(self): - return self.__dict__['data'] + return self._data def _set_data(self, data): - d = self.__dict__ - d['data'] = d['nodeValue'] = data + self._data = data - _get_nodeValue = _get_data - _set_nodeValue = _set_data - - def __setattr__(self, name, value): - if name == "data" or name == "nodeValue": - self.__dict__['data'] = self.__dict__['nodeValue'] = value - else: - self.__dict__[name] = value + data = nodeValue = property(_get_data, _set_data) def __repr__(self): data = self.data @@ -962,11 +1019,11 @@ def __repr__(self): def substringData(self, offset, count): if offset < 0: - raise xml2.dom.IndexSizeErr("offset cannot be negative") + raise xml.dom.IndexSizeErr("offset cannot be negative") if offset >= len(self.data): - raise xml2.dom.IndexSizeErr("offset cannot be beyond end of data") + raise xml.dom.IndexSizeErr("offset cannot be beyond end of data") if count < 0: - raise xml2.dom.IndexSizeErr("count cannot be negative") + raise xml.dom.IndexSizeErr("count cannot be negative") return self.data[offset:offset+count] def appendData(self, arg): @@ -974,30 +1031,30 @@ def appendData(self, arg): def insertData(self, offset, arg): if offset < 0: - raise xml2.dom.IndexSizeErr("offset cannot be negative") + raise xml.dom.IndexSizeErr("offset cannot be negative") if offset >= len(self.data): - raise xml2.dom.IndexSizeErr("offset cannot be beyond end of data") + raise xml.dom.IndexSizeErr("offset cannot be beyond end of data") if arg: self.data = "%s%s%s" % ( self.data[:offset], arg, self.data[offset:]) def deleteData(self, offset, count): if offset < 0: - raise xml2.dom.IndexSizeErr("offset cannot be negative") + raise xml.dom.IndexSizeErr("offset cannot be negative") if offset >= len(self.data): - raise xml2.dom.IndexSizeErr("offset cannot be beyond end of data") + raise xml.dom.IndexSizeErr("offset cannot be beyond end of data") if count < 0: - raise xml2.dom.IndexSizeErr("count cannot be negative") + raise xml.dom.IndexSizeErr("count cannot be negative") if count: self.data = self.data[:offset] + self.data[offset+count:] def replaceData(self, offset, count, arg): if offset < 0: - raise xml2.dom.IndexSizeErr("offset cannot be negative") + raise xml.dom.IndexSizeErr("offset cannot be negative") if offset >= len(self.data): - raise xml2.dom.IndexSizeErr("offset cannot be beyond end of data") + raise xml.dom.IndexSizeErr("offset cannot be beyond end of data") if count < 0: - raise xml2.dom.IndexSizeErr("count cannot be negative") + raise xml.dom.IndexSizeErr("count cannot be negative") if count: self.data = "%s%s%s" % ( self.data[:offset], arg, self.data[offset+count:]) @@ -1006,10 +1063,7 @@ def replaceData(self, offset, count, arg): class Text(CharacterData): - # Make sure we don't add an instance __dict__ if we don't already - # have one, at least when that's possible: - # XXX this does not work, CharacterData is an old-style class - # __slots__ = () + __slots__ = () nodeType = Node.TEXT_NODE nodeName = "#text" @@ -1017,7 +1071,7 @@ class Text(CharacterData): def splitText(self, offset): if offset < 0 or offset > len(self.data): - raise xml2.dom.IndexSizeErr("illegal offset value") + raise xml.dom.IndexSizeErr("illegal offset value") newText = self.__class__() newText.data = self.data[offset:] newText.ownerDocument = self.ownerDocument @@ -1031,7 +1085,7 @@ def splitText(self, offset): return newText def writexml(self, writer, indent="", addindent="", newl=""): - _write_data(writer, "%s%s%s"%(indent, self.data, newl)) + _write_data(writer, "%s%s%s" % (indent, self.data, newl)) # DOM Level 3 (WD 9 April 2002) @@ -1076,9 +1130,7 @@ def replaceWholeText(self, content): else: break if content: - d = self.__dict__ - d['data'] = content - d['nodeValue'] = content + self.data = content return self else: return None @@ -1119,12 +1171,13 @@ def _get_containing_entref(node): return None -class Comment(Childless, CharacterData): +class Comment(CharacterData): nodeType = Node.COMMENT_NODE nodeName = "#comment" def __init__(self, data): - self.data = self.nodeValue = data + CharacterData.__init__(self) + self._data = data def writexml(self, writer, indent="", addindent="", newl=""): if "--" in self.data: @@ -1133,10 +1186,7 @@ def writexml(self, writer, indent="", addindent="", newl=""): class CDATASection(Text): - # Make sure we don't add an instance __dict__ if we don't already - # have one, at least when that's possible: - # XXX this does not work, Text is an old-style class - # __slots__ = () + __slots__ = () nodeType = Node.CDATA_SECTION_NODE nodeName = "#cdata-section" @@ -1176,7 +1226,7 @@ def __getitem__(self, name_or_tuple): else: node = self.getNamedItem(name_or_tuple) if node is None: - raise KeyError, name_or_tuple + raise KeyError(name_or_tuple) return node def item(self, index): @@ -1188,19 +1238,19 @@ def item(self, index): return None def removeNamedItem(self, name): - raise xml2.dom.NoModificationAllowedErr( + raise xml.dom.NoModificationAllowedErr( "NamedNodeMap instance is read-only") def removeNamedItemNS(self, namespaceURI, localName): - raise xml2.dom.NoModificationAllowedErr( + raise xml.dom.NoModificationAllowedErr( "NamedNodeMap instance is read-only") def setNamedItem(self, node): - raise xml2.dom.NoModificationAllowedErr( + raise xml.dom.NoModificationAllowedErr( "NamedNodeMap instance is read-only") def setNamedItemNS(self, node): - raise xml2.dom.NoModificationAllowedErr( + raise xml.dom.NoModificationAllowedErr( "NamedNodeMap instance is read-only") def __getstate__(self): @@ -1216,8 +1266,7 @@ def __setstate__(self, state): class Identified: """Mix-in class that supports the publicId and systemId attributes.""" - # XXX this does not work, this is an old-style class - # __slots__ = 'publicId', 'systemId' + __slots__ = 'publicId', 'systemId' def _identified_mixin_init(self, publicId, systemId): self.publicId = publicId @@ -1254,7 +1303,7 @@ def cloneNode(self, deep): clone = DocumentType(None) clone.name = self.name clone.nodeName = self.name - operation = xml2.dom.UserDataHandler.NODE_CLONED + operation = xml.dom.UserDataHandler.NODE_CLONED if deep: clone.entities._seq = [] clone.notations._seq = [] @@ -1269,7 +1318,7 @@ def cloneNode(self, deep): entity.encoding = e.encoding entity.version = e.version clone.entities._seq.append(entity) - e._call_user_data_handler(operation, n, entity) + e._call_user_data_handler(operation, e, entity) self._call_user_data_handler(operation, self, clone) return clone else: @@ -1314,19 +1363,19 @@ def _get_version(self): return self.version def appendChild(self, newChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "cannot append children to an entity node") def insertBefore(self, newChild, refChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "cannot insert children below an entity node") def removeChild(self, oldChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "cannot remove children from an entity node") def replaceChild(self, newChild, oldChild): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "cannot replace children of an entity node") class Notation(Identified, Childless, Node): @@ -1356,7 +1405,7 @@ def hasFeature(self, feature, version): def createDocument(self, namespaceURI, qualifiedName, doctype): if doctype and doctype.parentNode is not None: - raise xml2.dom.WrongDocumentErr( + raise xml.dom.WrongDocumentErr( "doctype object owned by another DOM tree") doc = self._create_document() @@ -1377,15 +1426,15 @@ def createDocument(self, namespaceURI, qualifiedName, doctype): # Null the document is returned without a document element # Otherwise if doctype or namespaceURI are not None # Then we go back to the above problem - raise xml2.dom.InvalidCharacterErr("Element with no name") + raise xml.dom.InvalidCharacterErr("Element with no name") if add_root_element: prefix, localname = _nssplit(qualifiedName) if prefix == "xml" \ and namespaceURI != "http://www.w3.org/XML/1998/namespace": - raise xml2.dom.NamespaceErr("illegal use of 'xml' prefix") + raise xml.dom.NamespaceErr("illegal use of 'xml' prefix") if prefix and not namespaceURI: - raise xml2.dom.NamespaceErr( + raise xml.dom.NamespaceErr( "illegal use of prefix without namespaces") element = doc.createElementNS(namespaceURI, qualifiedName) if doctype: @@ -1468,18 +1517,19 @@ def _clear_id_cache(node): node.ownerDocument._id_search_stack= None class Document(Node, DocumentLS): + __slots__ = ('_elem_info', 'doctype', + '_id_search_stack', 'childNodes', '_id_cache') _child_node_types = (Node.ELEMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE, Node.COMMENT_NODE, Node.DOCUMENT_TYPE_NODE) + implementation = DOMImplementation() nodeType = Node.DOCUMENT_NODE nodeName = "#document" nodeValue = None attributes = None - doctype = None parentNode = None previousSibling = nextSibling = None - implementation = DOMImplementation() # Document attributes from Level 3 (WD 9 April 2002) @@ -1494,6 +1544,7 @@ class Document(Node, DocumentLS): _magic_id_count = 0 def __init__(self): + self.doctype = None self.childNodes = NodeList() # mapping of (namespaceURI, localName) -> ElementInfo # and tagName -> ElementInfo @@ -1534,7 +1585,7 @@ def _get_version(self): def appendChild(self, node): if node.nodeType not in self._child_node_types: - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "%s cannot be child of %s" % (repr(node), repr(self))) if node.parentNode is not None: # This needs to be done before the next test since this @@ -1544,7 +1595,7 @@ def appendChild(self, node): if node.nodeType == Node.ELEMENT_NODE \ and self._get_documentElement(): - raise xml2.dom.HierarchyRequestErr( + raise xml.dom.HierarchyRequestErr( "two document elements disallowed") return Node.appendChild(self, node) @@ -1552,7 +1603,7 @@ def removeChild(self, oldChild): try: self.childNodes.remove(oldChild) except ValueError: - raise xml2.dom.NotFoundErr() + raise xml.dom.NotFoundErr() oldChild.nextSibling = oldChild.previousSibling = None oldChild.parentNode = None if self.documentElement is oldChild: @@ -1588,7 +1639,7 @@ def cloneNode(self, deep): assert clone.doctype is None clone.doctype = childclone childclone.parentNode = clone - self._call_user_data_handler(xml2.dom.UserDataHandler.NODE_CLONED, + self._call_user_data_handler(xml.dom.UserDataHandler.NODE_CLONED, self, clone) return clone @@ -1603,16 +1654,16 @@ def createElement(self, tagName): return e def createTextNode(self, data): - if not isinstance(data, StringTypes): - raise TypeError, "node contents must be a string" + if not isinstance(data, str): + raise TypeError("node contents must be a string") t = Text() t.data = data t.ownerDocument = self return t def createCDATASection(self, data): - if not isinstance(data, StringTypes): - raise TypeError, "node contents must be a string" + if not isinstance(data, str): + raise TypeError("node contents must be a string") c = CDATASection() c.data = data c.ownerDocument = self @@ -1730,17 +1781,17 @@ def isSupported(self, feature, version): def importNode(self, node, deep): if node.nodeType == Node.DOCUMENT_NODE: - raise xml2.dom.NotSupportedErr("cannot import document nodes") + raise xml.dom.NotSupportedErr("cannot import document nodes") elif node.nodeType == Node.DOCUMENT_TYPE_NODE: - raise xml2.dom.NotSupportedErr("cannot import document type nodes") + raise xml.dom.NotSupportedErr("cannot import document type nodes") return _clone_node(node, deep, self) - def writexml(self, writer, indent="", addindent="", newl="", - encoding = None): + def writexml(self, writer, indent="", addindent="", newl="", encoding=None): if encoding is None: writer.write(''+newl) else: - writer.write('%s' % (encoding, newl)) + writer.write('%s' % ( + encoding, newl)) for node in self.childNodes: node.writexml(writer, indent, addindent, newl) @@ -1748,24 +1799,24 @@ def writexml(self, writer, indent="", addindent="", newl="", def renameNode(self, n, namespaceURI, name): if n.ownerDocument is not self: - raise xml2.dom.WrongDocumentErr( + raise xml.dom.WrongDocumentErr( "cannot rename nodes from other documents;\n" "expected %s,\nfound %s" % (self, n.ownerDocument)) if n.nodeType not in (Node.ELEMENT_NODE, Node.ATTRIBUTE_NODE): - raise xml2.dom.NotSupportedErr( + raise xml.dom.NotSupportedErr( "renameNode() only applies to element and attribute nodes") if namespaceURI != EMPTY_NAMESPACE: if ':' in name: prefix, localName = name.split(':', 1) if ( prefix == "xmlns" and namespaceURI != xml.dom.XMLNS_NAMESPACE): - raise xml2.dom.NamespaceErr( + raise xml.dom.NamespaceErr( "illegal use of 'xmlns' prefix") else: if ( name == "xmlns" and namespaceURI != xml.dom.XMLNS_NAMESPACE and n.nodeType == Node.ATTRIBUTE_NODE): - raise xml2.dom.NamespaceErr( + raise xml.dom.NamespaceErr( "illegal use of the 'xmlns' attribute") prefix = None localName = name @@ -1779,17 +1830,15 @@ def renameNode(self, n, namespaceURI, name): element.removeAttributeNode(n) else: element = None - # avoid __setattr__ - d = n.__dict__ - d['prefix'] = prefix - d['localName'] = localName - d['namespaceURI'] = namespaceURI - d['nodeName'] = name + n.prefix = prefix + n._localName = localName + n.namespaceURI = namespaceURI + n.nodeName = name if n.nodeType == Node.ELEMENT_NODE: - d['tagName'] = name + n.tagName = name else: # attribute node - d['name'] = name + n.name = name if element is not None: element.setAttributeNode(n) if is_id: @@ -1872,12 +1921,12 @@ def _clone_node(node, deep, newOwnerDocument): entity.ownerDocument = newOwnerDocument clone.entities._seq.append(entity) if hasattr(e, '_call_user_data_handler'): - e._call_user_data_handler(operation, n, entity) + e._call_user_data_handler(operation, e, entity) else: # Note the cloning of Document and DocumentType nodes is # implementation specific. minidom handles those cases # directly in the cloneNode() methods. - raise xml2.dom.NotSupportedErr("Cannot clone node %s" % repr(node)) + raise xml.dom.NotSupportedErr("Cannot clone node %s" % repr(node)) # Check for _call_user_data_handler() since this could conceivably # used with other DOM implementations (one of the FourThought @@ -1895,11 +1944,6 @@ def _nssplit(qualifiedName): return (None, fields[0]) -def _get_StringIO(): - # we can't use cStringIO since it doesn't support Unicode strings - from StringIO import StringIO - return StringIO() - def _do_pulldom_parse(func, args, kwargs): events = func(*args, **kwargs) toktype, rootNode = events.getEvent() @@ -1910,26 +1954,26 @@ def _do_pulldom_parse(func, args, kwargs): def parse(file, parser=None, bufsize=None): """Parse a file into a DOM by filename or file object.""" if parser is None and not bufsize: - from xml2.dom import expatbuilder + from xml.dom import expatbuilder return expatbuilder.parse(file) else: - from xml2.dom import pulldom + from xml.dom import pulldom return _do_pulldom_parse(pulldom.parse, (file,), {'parser': parser, 'bufsize': bufsize}) def parseString(string, parser=None): """Parse a file into a DOM from a string.""" if parser is None: - from xml2.dom import expatbuilder + from xml.dom import expatbuilder return expatbuilder.parseString(string) else: - from xml2.dom import pulldom + from xml.dom import pulldom return _do_pulldom_parse(pulldom.parseString, (string,), {'parser': parser}) def getDOMImplementation(features=None): if features: - if isinstance(features, StringTypes): + if isinstance(features, str): features = domreg._parse_feature_string(features) for f, v in features: if not Document.implementation.hasFeature(f, v): diff --git a/addon/xml2/dom/pulldom.py b/addon/globalPlugins/readFeeds/xml/dom/pulldom.py similarity index 96% rename from addon/xml2/dom/pulldom.py rename to addon/globalPlugins/readFeeds/xml/dom/pulldom.py index 18f49b50..43504f76 100644 --- a/addon/xml2/dom/pulldom.py +++ b/addon/globalPlugins/readFeeds/xml/dom/pulldom.py @@ -1,11 +1,5 @@ import xml.sax import xml.sax.handler -import types - -try: - _StringTypes = [types.StringType, types.UnicodeType] -except AttributeError: - _StringTypes = [types.StringType] START_ELEMENT = "START_ELEMENT" END_ELEMENT = "END_ELEMENT" @@ -201,7 +195,7 @@ def clear(self): class ErrorHandler: def warning(self, exception): - print exception + print(exception) def error(self, exception): raise exception def fatalError(self, exception): @@ -228,7 +222,7 @@ def __getitem__(self, pos): return rc raise IndexError - def next(self): + def __next__(self): rc = self.getEvent() if rc: return rc @@ -330,8 +324,8 @@ def characters(self, chars): def parse(stream_or_string, parser=None, bufsize=None): if bufsize is None: bufsize = default_bufsize - if type(stream_or_string) in _StringTypes: - stream = open(stream_or_string) + if isinstance(stream_or_string, str): + stream = open(stream_or_string, 'rb') else: stream = stream_or_string if not parser: @@ -339,10 +333,7 @@ def parse(stream_or_string, parser=None, bufsize=None): return DOMEventStream(stream, parser, bufsize) def parseString(string, parser=None): - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO + from io import StringIO bufsize = len(string) buf = StringIO(string) diff --git a/addon/xml2/dom/xmlbuilder.py b/addon/globalPlugins/readFeeds/xml/dom/xmlbuilder.py similarity index 92% rename from addon/xml2/dom/xmlbuilder.py rename to addon/globalPlugins/readFeeds/xml/dom/xmlbuilder.py index e041a96f..213ab145 100644 --- a/addon/xml2/dom/xmlbuilder.py +++ b/addon/globalPlugins/readFeeds/xml/dom/xmlbuilder.py @@ -1,9 +1,10 @@ """Implementation of the DOM Level 3 'LS-Load' feature.""" import copy -import xml2.dom +import warnings +import xml.dom -from xml2.dom.NodeFilter import NodeFilter +from xml.dom.NodeFilter import NodeFilter __all__ = ["DOMBuilder", "DOMEntityResolver", "DOMInputSource"] @@ -78,13 +79,13 @@ def setFeature(self, name, state): try: settings = self._settings[(_name_xform(name), state)] except KeyError: - raise xml2dom.NotSupportedErr( - "unsupported feature: %r" % (name,)) + raise xml.dom.NotSupportedErr( + "unsupported feature: %r" % (name,)) from None else: for name, value in settings: setattr(self._options, name, value) else: - raise xml2dom.NotFoundErr("unknown feature: " + repr(name)) + raise xml.dom.NotFoundErr("unknown feature: " + repr(name)) def supportsFeature(self, name): return hasattr(self._options, _name_xform(name)) @@ -175,7 +176,7 @@ def getFeature(self, name): or options.create_entity_ref_nodes or options.entities or options.cdata_sections)) - raise xml2dom.NotFoundErr("feature %s not known" % repr(name)) + raise xml.dom.NotFoundErr("feature %s not known" % repr(name)) def parseURI(self, uri): if self.entityResolver: @@ -190,8 +191,8 @@ def parse(self, input): options.errorHandler = self.errorHandler fp = input.byteStream if fp is None and options.systemId: - import urllib2 - fp = urllib2.urlopen(input.systemId) + import urllib.request + fp = urllib.request.urlopen(input.systemId) return self._parse_bytestream(fp, options) def parseWithContext(self, input, cnode, action): @@ -200,8 +201,8 @@ def parseWithContext(self, input, cnode, action): raise NotImplementedError("Haven't written this yet...") def _parse_bytestream(self, stream, options): - import xml2dom.expatbuilder - builder = xml2dom.expatbuilder.makeBuilder(options) + import xml.dom.expatbuilder + builder = xml.dom.expatbuilder.makeBuilder(options) return builder.parseFile(stream) @@ -223,14 +224,14 @@ def resolveEntity(self, publicId, systemId): source.encoding = self._guess_media_encoding(source) # determine the base URI is we can - import posixpath, urlparse - parts = urlparse.urlparse(systemId) + import posixpath, urllib.parse + parts = urllib.parse.urlparse(systemId) scheme, netloc, path, params, query, fragment = parts # XXX should we check the scheme here as well? if path and not path.endswith("/"): path = posixpath.dirname(path) + "/" parts = scheme, netloc, path, params, query, fragment - source.baseURI = urlparse.urlunparse(parts) + source.baseURI = urllib.parse.urlunparse(parts) return source @@ -242,8 +243,8 @@ def _get_opener(self): return self._opener def _create_opener(self): - import urllib2 - return urllib2.build_opener() + import urllib.request + return urllib.request.build_opener() def _guess_media_encoding(self, source): info = source.byteStream.info() @@ -334,13 +335,14 @@ def startContainer(self, element): class DocumentLS: """Mixin to create documents that conform to the load/save spec.""" - async = False + async_ = False def _get_async(self): return False - def _set_async(self, async): - if async: - raise xml2dom.NotSupportedErr( + + def _set_async(self, flag): + if flag: + raise xml.dom.NotSupportedErr( "asynchronous document loading is not supported") def abort(self): @@ -359,7 +361,7 @@ def saveXML(self, snode): if snode is None: snode = self elif snode.ownerDocument is not self: - raise xml2dom.WrongDocumentErr() + raise xml.dom.WrongDocumentErr() return snode.toxml() @@ -369,12 +371,12 @@ class DOMImplementationLS: def createDOMBuilder(self, mode, schemaType): if schemaType is not None: - raise xml2dom.NotSupportedErr( + raise xml.dom.NotSupportedErr( "schemaType not yet supported") if mode == self.MODE_SYNCHRONOUS: return DOMBuilder() if mode == self.MODE_ASYNCHRONOUS: - raise xml2dom.NotSupportedErr( + raise xml.dom.NotSupportedErr( "asynchronous builders are not supported") raise ValueError("unknown value for mode") diff --git a/addon/xml2/etree/ElementInclude.py b/addon/globalPlugins/readFeeds/xml/etree/ElementInclude.py similarity index 90% rename from addon/xml2/etree/ElementInclude.py rename to addon/globalPlugins/readFeeds/xml/etree/ElementInclude.py index 84fd7548..963470e3 100644 --- a/addon/xml2/etree/ElementInclude.py +++ b/addon/globalPlugins/readFeeds/xml/etree/ElementInclude.py @@ -67,22 +67,22 @@ class FatalIncludeError(SyntaxError): # # @param href Resource reference. # @param parse Parse mode. Either "xml" or "text". -# @param encoding Optional text encoding. +# @param encoding Optional text encoding (UTF-8 by default for "text"). # @return The expanded resource. If the parse mode is "xml", this # is an ElementTree instance. If the parse mode is "text", this # is a Unicode string. If the loader fails, it can return None -# or raise an IOError exception. -# @throws IOError If the loader fails to load the resource. +# or raise an OSError exception. +# @throws OSError If the loader fails to load the resource. def default_loader(href, parse, encoding=None): - file = open(href) if parse == "xml": - data = ElementTree.parse(file).getroot() + with open(href, 'rb') as file: + data = ElementTree.parse(file).getroot() else: - data = file.read() - if encoding: - data = data.decode(encoding) - file.close() + if not encoding: + encoding = 'UTF-8' + with open(href, 'r', encoding=encoding) as file: + data = file.read() return data ## @@ -94,7 +94,7 @@ def default_loader(href, parse, encoding=None): # that implements the same interface as default_loader. # @throws FatalIncludeError If the function fails to include a given # resource, or if the tree contains malformed XInclude elements. -# @throws IOError If the function fails to load a given resource. +# @throws OSError If the function fails to load a given resource. def include(elem, loader=None): if loader is None: diff --git a/addon/xml2/etree/ElementPath.py b/addon/globalPlugins/readFeeds/xml/etree/ElementPath.py similarity index 82% rename from addon/xml2/etree/ElementPath.py rename to addon/globalPlugins/readFeeds/xml/etree/ElementPath.py index 4a626d79..ef32917b 100644 --- a/addon/xml2/etree/ElementPath.py +++ b/addon/globalPlugins/readFeeds/xml/etree/ElementPath.py @@ -59,15 +59,15 @@ import re xpath_tokenizer_re = re.compile( - "(" - "'[^']*'|\"[^\"]*\"|" - "::|" - "//?|" - "\.\.|" - "\(\)|" - "[/.*:\[\]\(\)@=])|" - "((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|" - "\s+" + r"(" + r"'[^']*'|\"[^\"]*\"|" + r"::|" + r"//?|" + r"\.\.|" + r"\(\)|" + r"[/.*:\[\]\(\)@=])|" + r"((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|" + r"\s+" ) def xpath_tokenizer(pattern, namespaces=None): @@ -80,7 +80,7 @@ def xpath_tokenizer(pattern, namespaces=None): raise KeyError yield token[0], "{%s}%s" % (namespaces[prefix], uri) except KeyError: - raise SyntaxError("prefix %r not found in prefix map" % prefix) + raise SyntaxError("prefix %r not found in prefix map" % prefix) from None else: yield token @@ -105,18 +105,19 @@ def select(context, result): def prepare_star(next, token): def select(context, result): for elem in result: - for e in elem: - yield e + yield from elem return select def prepare_self(next, token): def select(context, result): - for elem in result: - yield elem + yield from result return select def prepare_descendant(next, token): - token = next() + try: + token = next() + except StopIteration: + return if token[0] == "*": tag = "*" elif not token[0]: @@ -150,9 +151,15 @@ def prepare_predicate(next, token): signature = [] predicate = [] while 1: - token = next() + try: + token = next() + except StopIteration: + return if token[0] == "]": break + if token == ('', ''): + # ignore whitespace + continue if token[0] and token[0][:1] in "'\"": token = "'", token[0][1:-1] signature.append(token[0] or "-") @@ -176,7 +183,7 @@ def select(context, result): if elem.get(key) == value: yield elem return select - if signature == "-" and not re.match("\d+$", predicate[0]): + if signature == "-" and not re.match(r"\-?\d+$", predicate[0]): # [tag] tag = predicate[0] def select(context, result): @@ -184,21 +191,30 @@ def select(context, result): if elem.find(tag) is not None: yield elem return select - if signature == "-='" and not re.match("\d+$", predicate[0]): - # [tag='value'] + if signature == ".='" or (signature == "-='" and not re.match(r"\-?\d+$", predicate[0])): + # [.='value'] or [tag='value'] tag = predicate[0] value = predicate[-1] - def select(context, result): - for elem in result: - for e in elem.findall(tag): - if "".join(e.itertext()) == value: + if tag: + def select(context, result): + for elem in result: + for e in elem.findall(tag): + if "".join(e.itertext()) == value: + yield elem + break + else: + def select(context, result): + for elem in result: + if "".join(elem.itertext()) == value: yield elem - break return select if signature == "-" or signature == "-()" or signature == "-()-": # [index] or [last()] or [last()-index] if signature == "-": + # [index] index = int(predicate[0]) - 1 + if index < 0: + raise SyntaxError("XPath position >= 1 expected") else: if predicate[0] != "last": raise SyntaxError("unsupported function") @@ -207,6 +223,8 @@ def select(context, result): index = int(predicate[2]) - 1 except ValueError: raise SyntaxError("unsupported expression") + if index > -2: + raise SyntaxError("XPath offset from last() must be negative") else: index = -1 def select(context, result): @@ -246,30 +264,35 @@ def __init__(self, root): def iterfind(elem, path, namespaces=None): # compile selector pattern + cache_key = (path, None if namespaces is None + else tuple(sorted(namespaces.items()))) if path[-1:] == "/": path = path + "*" # implicit all (FIXME: keep this?) try: - selector = _cache[path] + selector = _cache[cache_key] except KeyError: if len(_cache) > 100: _cache.clear() if path[:1] == "/": raise SyntaxError("cannot use absolute path on element") - next = iter(xpath_tokenizer(path, namespaces)).next - token = next() + next = iter(xpath_tokenizer(path, namespaces)).__next__ + try: + token = next() + except StopIteration: + return selector = [] while 1: try: selector.append(ops[token[0]](next, token)) except StopIteration: - raise SyntaxError("invalid path") + raise SyntaxError("invalid path") from None try: token = next() if token[0] == "/": token = next() except StopIteration: break - _cache[path] = selector + _cache[cache_key] = selector # execute selector pattern result = [elem] context = _SelectorContext(elem) @@ -281,10 +304,7 @@ def iterfind(elem, path, namespaces=None): # Find first matching object. def find(elem, path, namespaces=None): - try: - return iterfind(elem, path, namespaces).next() - except StopIteration: - return None + return next(iterfind(elem, path, namespaces), None) ## # Find all matching objects. @@ -297,7 +317,7 @@ def findall(elem, path, namespaces=None): def findtext(elem, path, default=None, namespaces=None): try: - elem = iterfind(elem, path, namespaces).next() + elem = next(iterfind(elem, path, namespaces)) return elem.text or "" except StopIteration: return default diff --git a/addon/globalPlugins/readFeeds/xml/etree/ElementTree.py b/addon/globalPlugins/readFeeds/xml/etree/ElementTree.py new file mode 100644 index 00000000..87277045 --- /dev/null +++ b/addon/globalPlugins/readFeeds/xml/etree/ElementTree.py @@ -0,0 +1,1662 @@ +"""Lightweight XML support for Python. + + XML is an inherently hierarchical data format, and the most natural way to + represent it is with a tree. This module has two classes for this purpose: + + 1. ElementTree represents the whole XML document as a tree and + + 2. Element represents a single node in this tree. + + Interactions with the whole document (reading and writing to/from files) are + usually done on the ElementTree level. Interactions with a single XML element + and its sub-elements are done on the Element level. + + Element is a flexible container object designed to store hierarchical data + structures in memory. It can be described as a cross between a list and a + dictionary. Each Element has a number of properties associated with it: + + 'tag' - a string containing the element's name. + + 'attributes' - a Python dictionary storing the element's attributes. + + 'text' - a string containing the element's text content. + + 'tail' - an optional string containing text after the element's end tag. + + And a number of child elements stored in a Python sequence. + + To create an element instance, use the Element constructor, + or the SubElement factory function. + + You can also use the ElementTree class to wrap an element structure + and convert it to and from XML. + +""" + +#--------------------------------------------------------------------- +# Licensed to PSF under a Contributor Agreement. +# See http://www.python.org/psf/license for licensing details. +# +# ElementTree +# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved. +# +# fredrik@pythonware.com +# http://www.pythonware.com +# -------------------------------------------------------------------- +# The ElementTree toolkit is +# +# Copyright (c) 1999-2008 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +__all__ = [ + # public symbols + "Comment", + "dump", + "Element", "ElementTree", + "fromstring", "fromstringlist", + "iselement", "iterparse", + "parse", "ParseError", + "PI", "ProcessingInstruction", + "QName", + "SubElement", + "tostring", "tostringlist", + "TreeBuilder", + "VERSION", + "XML", "XMLID", + "XMLParser", "XMLPullParser", + "register_namespace", + ] + +VERSION = "1.3.0" + +import sys +import re +import warnings +import io +import collections +import collections.abc +import contextlib + +from . import ElementPath + + +class ParseError(SyntaxError): + """An error when parsing an XML document. + + In addition to its exception value, a ParseError contains + two extra attributes: + 'code' - the specific exception code + 'position' - the line and column of the error + + """ + pass + +# -------------------------------------------------------------------- + + +def iselement(element): + """Return True if *element* appears to be an Element.""" + return hasattr(element, 'tag') + + +class Element: + """An XML element. + + This class is the reference implementation of the Element interface. + + An element's length is its number of subelements. That means if you + want to check if an element is truly empty, you should check BOTH + its length AND its text attribute. + + The element tag, attribute names, and attribute values can be either + bytes or strings. + + *tag* is the element name. *attrib* is an optional dictionary containing + element attributes. *extra* are additional element attributes given as + keyword arguments. + + Example form: + text...tail + + """ + + tag = None + """The element's name.""" + + attrib = None + """Dictionary of the element's attributes.""" + + text = None + """ + Text before first subelement. This is either a string or the value None. + Note that if there is no text, this attribute may be either + None or the empty string, depending on the parser. + + """ + + tail = None + """ + Text after this element's end tag, but before the next sibling element's + start tag. This is either a string or the value None. Note that if there + was no text, this attribute may be either None or an empty string, + depending on the parser. + + """ + + def __init__(self, tag, attrib={}, **extra): + if not isinstance(attrib, dict): + raise TypeError("attrib must be dict, not %s" % ( + attrib.__class__.__name__,)) + attrib = attrib.copy() + attrib.update(extra) + self.tag = tag + self.attrib = attrib + self._children = [] + + def __repr__(self): + return "<%s %r at %#x>" % (self.__class__.__name__, self.tag, id(self)) + + def makeelement(self, tag, attrib): + """Create a new element with the same type. + + *tag* is a string containing the element name. + *attrib* is a dictionary containing the element attributes. + + Do not call this method, use the SubElement factory function instead. + + """ + return self.__class__(tag, attrib) + + def copy(self): + """Return copy of current element. + + This creates a shallow copy. Subelements will be shared with the + original tree. + + """ + elem = self.makeelement(self.tag, self.attrib) + elem.text = self.text + elem.tail = self.tail + elem[:] = self + return elem + + def __len__(self): + return len(self._children) + + def __bool__(self): + warnings.warn( + "The behavior of this method will change in future versions. " + "Use specific 'len(elem)' or 'elem is not None' test instead.", + FutureWarning, stacklevel=2 + ) + return len(self._children) != 0 # emulate old behaviour, for now + + def __getitem__(self, index): + return self._children[index] + + def __setitem__(self, index, element): + # if isinstance(index, slice): + # for elt in element: + # assert iselement(elt) + # else: + # assert iselement(element) + self._children[index] = element + + def __delitem__(self, index): + del self._children[index] + + def append(self, subelement): + """Add *subelement* to the end of this element. + + The new element will appear in document order after the last existing + subelement (or directly after the text, if it's the first subelement), + but before the end tag for this element. + + """ + self._assert_is_element(subelement) + self._children.append(subelement) + + def extend(self, elements): + """Append subelements from a sequence. + + *elements* is a sequence with zero or more elements. + + """ + for element in elements: + self._assert_is_element(element) + self._children.extend(elements) + + def insert(self, index, subelement): + """Insert *subelement* at position *index*.""" + self._assert_is_element(subelement) + self._children.insert(index, subelement) + + def _assert_is_element(self, e): + # Need to refer to the actual Python implementation, not the + # shadowing C implementation. + if not isinstance(e, _Element_Py): + raise TypeError('expected an Element, not %s' % type(e).__name__) + + def remove(self, subelement): + """Remove matching subelement. + + Unlike the find methods, this method compares elements based on + identity, NOT ON tag value or contents. To remove subelements by + other means, the easiest way is to use a list comprehension to + select what elements to keep, and then use slice assignment to update + the parent element. + + ValueError is raised if a matching element could not be found. + + """ + # assert iselement(element) + self._children.remove(subelement) + + def getchildren(self): + """(Deprecated) Return all subelements. + + Elements are returned in document order. + + """ + warnings.warn( + "This method will be removed in future versions. " + "Use 'list(elem)' or iteration over elem instead.", + DeprecationWarning, stacklevel=2 + ) + return self._children + + def find(self, path, namespaces=None): + """Find first matching element by tag name or path. + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return the first matching element, or None if no element was found. + + """ + return ElementPath.find(self, path, namespaces) + + def findtext(self, path, default=None, namespaces=None): + """Find text for first matching element by tag name or path. + + *path* is a string having either an element tag or an XPath, + *default* is the value to return if the element was not found, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return text content of first matching element, or default value if + none was found. Note that if an element is found having no text + content, the empty string is returned. + + """ + return ElementPath.findtext(self, path, default, namespaces) + + def findall(self, path, namespaces=None): + """Find all matching subelements by tag name or path. + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Returns list containing all matching elements in document order. + + """ + return ElementPath.findall(self, path, namespaces) + + def iterfind(self, path, namespaces=None): + """Find all matching subelements by tag name or path. + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return an iterable yielding all matching elements in document order. + + """ + return ElementPath.iterfind(self, path, namespaces) + + def clear(self): + """Reset element. + + This function removes all subelements, clears all attributes, and sets + the text and tail attributes to None. + + """ + self.attrib.clear() + self._children = [] + self.text = self.tail = None + + def get(self, key, default=None): + """Get element attribute. + + Equivalent to attrib.get, but some implementations may handle this a + bit more efficiently. *key* is what attribute to look for, and + *default* is what to return if the attribute was not found. + + Returns a string containing the attribute value, or the default if + attribute was not found. + + """ + return self.attrib.get(key, default) + + def set(self, key, value): + """Set element attribute. + + Equivalent to attrib[key] = value, but some implementations may handle + this a bit more efficiently. *key* is what attribute to set, and + *value* is the attribute value to set it to. + + """ + self.attrib[key] = value + + def keys(self): + """Get list of attribute names. + + Names are returned in an arbitrary order, just like an ordinary + Python dict. Equivalent to attrib.keys() + + """ + return self.attrib.keys() + + def items(self): + """Get element attributes as a sequence. + + The attributes are returned in arbitrary order. Equivalent to + attrib.items(). + + Return a list of (name, value) tuples. + + """ + return self.attrib.items() + + def iter(self, tag=None): + """Create tree iterator. + + The iterator loops over the element and all subelements in document + order, returning all elements with a matching tag. + + If the tree structure is modified during iteration, new or removed + elements may or may not be included. To get a stable set, use the + list() function on the iterator, and loop over the resulting list. + + *tag* is what tags to look for (default is to return all elements) + + Return an iterator containing all the matching elements. + + """ + if tag == "*": + tag = None + if tag is None or self.tag == tag: + yield self + for e in self._children: + yield from e.iter(tag) + + # compatibility + def getiterator(self, tag=None): + # Change for a DeprecationWarning in 1.4 + warnings.warn( + "This method will be removed in future versions. " + "Use 'elem.iter()' or 'list(elem.iter())' instead.", + PendingDeprecationWarning, stacklevel=2 + ) + return list(self.iter(tag)) + + def itertext(self): + """Create text iterator. + + The iterator loops over the element and all subelements in document + order, returning all inner text. + + """ + tag = self.tag + if not isinstance(tag, str) and tag is not None: + return + t = self.text + if t: + yield t + for e in self: + yield from e.itertext() + t = e.tail + if t: + yield t + + +def SubElement(parent, tag, attrib={}, **extra): + """Subelement factory which creates an element instance, and appends it + to an existing parent. + + The element tag, attribute names, and attribute values can be either + bytes or Unicode strings. + + *parent* is the parent element, *tag* is the subelements name, *attrib* is + an optional directory containing element attributes, *extra* are + additional attributes given as keyword arguments. + + """ + attrib = attrib.copy() + attrib.update(extra) + element = parent.makeelement(tag, attrib) + parent.append(element) + return element + + +def Comment(text=None): + """Comment element factory. + + This function creates a special element which the standard serializer + serializes as an XML comment. + + *text* is a string containing the comment string. + + """ + element = Element(Comment) + element.text = text + return element + + +def ProcessingInstruction(target, text=None): + """Processing Instruction element factory. + + This function creates a special element which the standard serializer + serializes as an XML comment. + + *target* is a string containing the processing instruction, *text* is a + string containing the processing instruction contents, if any. + + """ + element = Element(ProcessingInstruction) + element.text = target + if text: + element.text = element.text + " " + text + return element + +PI = ProcessingInstruction + + +class QName: + """Qualified name wrapper. + + This class can be used to wrap a QName attribute value in order to get + proper namespace handing on output. + + *text_or_uri* is a string containing the QName value either in the form + {uri}local, or if the tag argument is given, the URI part of a QName. + + *tag* is an optional argument which if given, will make the first + argument (text_or_uri) be interpreted as a URI, and this argument (tag) + be interpreted as a local name. + + """ + def __init__(self, text_or_uri, tag=None): + if tag: + text_or_uri = "{%s}%s" % (text_or_uri, tag) + self.text = text_or_uri + def __str__(self): + return self.text + def __repr__(self): + return '<%s %r>' % (self.__class__.__name__, self.text) + def __hash__(self): + return hash(self.text) + def __le__(self, other): + if isinstance(other, QName): + return self.text <= other.text + return self.text <= other + def __lt__(self, other): + if isinstance(other, QName): + return self.text < other.text + return self.text < other + def __ge__(self, other): + if isinstance(other, QName): + return self.text >= other.text + return self.text >= other + def __gt__(self, other): + if isinstance(other, QName): + return self.text > other.text + return self.text > other + def __eq__(self, other): + if isinstance(other, QName): + return self.text == other.text + return self.text == other + +# -------------------------------------------------------------------- + + +class ElementTree: + """An XML element hierarchy. + + This class also provides support for serialization to and from + standard XML. + + *element* is an optional root element node, + *file* is an optional file handle or file name of an XML file whose + contents will be used to initialize the tree with. + + """ + def __init__(self, element=None, file=None): + # assert element is None or iselement(element) + self._root = element # first node + if file: + self.parse(file) + + def getroot(self): + """Return root element of this tree.""" + return self._root + + def _setroot(self, element): + """Replace root element of this tree. + + This will discard the current contents of the tree and replace it + with the given element. Use with care! + + """ + # assert iselement(element) + self._root = element + + def parse(self, source, parser=None): + """Load external XML document into element tree. + + *source* is a file name or file object, *parser* is an optional parser + instance that defaults to XMLParser. + + ParseError is raised if the parser fails to parse the document. + + Returns the root element of the given source document. + + """ + close_source = False + if not hasattr(source, "read"): + source = open(source, "rb") + close_source = True + try: + if parser is None: + # If no parser was specified, create a default XMLParser + parser = XMLParser() + if hasattr(parser, '_parse_whole'): + # The default XMLParser, when it comes from an accelerator, + # can define an internal _parse_whole API for efficiency. + # It can be used to parse the whole source without feeding + # it with chunks. + self._root = parser._parse_whole(source) + return self._root + while True: + data = source.read(65536) + if not data: + break + parser.feed(data) + self._root = parser.close() + return self._root + finally: + if close_source: + source.close() + + def iter(self, tag=None): + """Create and return tree iterator for the root element. + + The iterator loops over all elements in this tree, in document order. + + *tag* is a string with the tag name to iterate over + (default is to return all elements). + + """ + # assert self._root is not None + return self._root.iter(tag) + + # compatibility + def getiterator(self, tag=None): + # Change for a DeprecationWarning in 1.4 + warnings.warn( + "This method will be removed in future versions. " + "Use 'tree.iter()' or 'list(tree.iter())' instead.", + PendingDeprecationWarning, stacklevel=2 + ) + return list(self.iter(tag)) + + def find(self, path, namespaces=None): + """Find first matching element by tag name or path. + + Same as getroot().find(path), which is Element.find() + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return the first matching element, or None if no element was found. + + """ + # assert self._root is not None + if path[:1] == "/": + path = "." + path + warnings.warn( + "This search is broken in 1.3 and earlier, and will be " + "fixed in a future version. If you rely on the current " + "behaviour, change it to %r" % path, + FutureWarning, stacklevel=2 + ) + return self._root.find(path, namespaces) + + def findtext(self, path, default=None, namespaces=None): + """Find first matching element by tag name or path. + + Same as getroot().findtext(path), which is Element.findtext() + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return the first matching element, or None if no element was found. + + """ + # assert self._root is not None + if path[:1] == "/": + path = "." + path + warnings.warn( + "This search is broken in 1.3 and earlier, and will be " + "fixed in a future version. If you rely on the current " + "behaviour, change it to %r" % path, + FutureWarning, stacklevel=2 + ) + return self._root.findtext(path, default, namespaces) + + def findall(self, path, namespaces=None): + """Find all matching subelements by tag name or path. + + Same as getroot().findall(path), which is Element.findall(). + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return list containing all matching elements in document order. + + """ + # assert self._root is not None + if path[:1] == "/": + path = "." + path + warnings.warn( + "This search is broken in 1.3 and earlier, and will be " + "fixed in a future version. If you rely on the current " + "behaviour, change it to %r" % path, + FutureWarning, stacklevel=2 + ) + return self._root.findall(path, namespaces) + + def iterfind(self, path, namespaces=None): + """Find all matching subelements by tag name or path. + + Same as getroot().iterfind(path), which is element.iterfind() + + *path* is a string having either an element tag or an XPath, + *namespaces* is an optional mapping from namespace prefix to full name. + + Return an iterable yielding all matching elements in document order. + + """ + # assert self._root is not None + if path[:1] == "/": + path = "." + path + warnings.warn( + "This search is broken in 1.3 and earlier, and will be " + "fixed in a future version. If you rely on the current " + "behaviour, change it to %r" % path, + FutureWarning, stacklevel=2 + ) + return self._root.iterfind(path, namespaces) + + def write(self, file_or_filename, + encoding=None, + xml_declaration=None, + default_namespace=None, + method=None, *, + short_empty_elements=True): + """Write element tree to a file as XML. + + Arguments: + *file_or_filename* -- file name or a file object opened for writing + + *encoding* -- the output encoding (default: US-ASCII) + + *xml_declaration* -- bool indicating if an XML declaration should be + added to the output. If None, an XML declaration + is added if encoding IS NOT either of: + US-ASCII, UTF-8, or Unicode + + *default_namespace* -- sets the default XML namespace (for "xmlns") + + *method* -- either "xml" (default), "html, "text", or "c14n" + + *short_empty_elements* -- controls the formatting of elements + that contain no content. If True (default) + they are emitted as a single self-closed + tag, otherwise they are emitted as a pair + of start/end tags + + """ + if not method: + method = "xml" + elif method not in _serialize: + raise ValueError("unknown method %r" % method) + if not encoding: + if method == "c14n": + encoding = "utf-8" + else: + encoding = "us-ascii" + enc_lower = encoding.lower() + with _get_writer(file_or_filename, enc_lower) as write: + if method == "xml" and (xml_declaration or + (xml_declaration is None and + enc_lower not in ("utf-8", "us-ascii", "unicode"))): + declared_encoding = encoding + if enc_lower == "unicode": + # Retrieve the default encoding for the xml declaration + import locale + declared_encoding = locale.getpreferredencoding() + write("\n" % ( + declared_encoding,)) + if method == "text": + _serialize_text(write, self._root) + else: + qnames, namespaces = _namespaces(self._root, default_namespace) + serialize = _serialize[method] + serialize(write, self._root, qnames, namespaces, + short_empty_elements=short_empty_elements) + + def write_c14n(self, file): + # lxml.etree compatibility. use output method instead + return self.write(file, method="c14n") + +# -------------------------------------------------------------------- +# serialization support + +@contextlib.contextmanager +def _get_writer(file_or_filename, encoding): + # returns text write method and release all resources after using + try: + write = file_or_filename.write + except AttributeError: + # file_or_filename is a file name + if encoding == "unicode": + file = open(file_or_filename, "w") + else: + file = open(file_or_filename, "w", encoding=encoding, + errors="xmlcharrefreplace") + with file: + yield file.write + else: + # file_or_filename is a file-like object + # encoding determines if it is a text or binary writer + if encoding == "unicode": + # use a text writer as is + yield write + else: + # wrap a binary writer with TextIOWrapper + with contextlib.ExitStack() as stack: + if isinstance(file_or_filename, io.BufferedIOBase): + file = file_or_filename + elif isinstance(file_or_filename, io.RawIOBase): + file = io.BufferedWriter(file_or_filename) + # Keep the original file open when the BufferedWriter is + # destroyed + stack.callback(file.detach) + else: + # This is to handle passed objects that aren't in the + # IOBase hierarchy, but just have a write method + file = io.BufferedIOBase() + file.writable = lambda: True + file.write = write + try: + # TextIOWrapper uses this methods to determine + # if BOM (for UTF-16, etc) should be added + file.seekable = file_or_filename.seekable + file.tell = file_or_filename.tell + except AttributeError: + pass + file = io.TextIOWrapper(file, + encoding=encoding, + errors="xmlcharrefreplace", + newline="\n") + # Keep the original file open when the TextIOWrapper is + # destroyed + stack.callback(file.detach) + yield file.write + +def _namespaces(elem, default_namespace=None): + # identify namespaces used in this tree + + # maps qnames to *encoded* prefix:local names + qnames = {None: None} + + # maps uri:s to prefixes + namespaces = {} + if default_namespace: + namespaces[default_namespace] = "" + + def add_qname(qname): + # calculate serialized qname representation + try: + if qname[:1] == "{": + uri, tag = qname[1:].rsplit("}", 1) + prefix = namespaces.get(uri) + if prefix is None: + prefix = _namespace_map.get(uri) + if prefix is None: + prefix = "ns%d" % len(namespaces) + if prefix != "xml": + namespaces[uri] = prefix + if prefix: + qnames[qname] = "%s:%s" % (prefix, tag) + else: + qnames[qname] = tag # default element + else: + if default_namespace: + # FIXME: can this be handled in XML 1.0? + raise ValueError( + "cannot use non-qualified names with " + "default_namespace option" + ) + qnames[qname] = qname + except TypeError: + _raise_serialization_error(qname) + + # populate qname and namespaces table + for elem in elem.iter(): + tag = elem.tag + if isinstance(tag, QName): + if tag.text not in qnames: + add_qname(tag.text) + elif isinstance(tag, str): + if tag not in qnames: + add_qname(tag) + elif tag is not None and tag is not Comment and tag is not PI: + _raise_serialization_error(tag) + for key, value in elem.items(): + if isinstance(key, QName): + key = key.text + if key not in qnames: + add_qname(key) + if isinstance(value, QName) and value.text not in qnames: + add_qname(value.text) + text = elem.text + if isinstance(text, QName) and text.text not in qnames: + add_qname(text.text) + return qnames, namespaces + +def _serialize_xml(write, elem, qnames, namespaces, + short_empty_elements, **kwargs): + tag = elem.tag + text = elem.text + if tag is Comment: + write("" % text) + elif tag is ProcessingInstruction: + write("" % text) + else: + tag = qnames[tag] + if tag is None: + if text: + write(_escape_cdata(text)) + for e in elem: + _serialize_xml(write, e, qnames, None, + short_empty_elements=short_empty_elements) + else: + write("<" + tag) + items = list(elem.items()) + if items or namespaces: + if namespaces: + for v, k in sorted(namespaces.items(), + key=lambda x: x[1]): # sort on prefix + if k: + k = ":" + k + write(" xmlns%s=\"%s\"" % ( + k, + _escape_attrib(v) + )) + for k, v in sorted(items): # lexical order + if isinstance(k, QName): + k = k.text + if isinstance(v, QName): + v = qnames[v.text] + else: + v = _escape_attrib(v) + write(" %s=\"%s\"" % (qnames[k], v)) + if text or len(elem) or not short_empty_elements: + write(">") + if text: + write(_escape_cdata(text)) + for e in elem: + _serialize_xml(write, e, qnames, None, + short_empty_elements=short_empty_elements) + write("") + else: + write(" />") + if elem.tail: + write(_escape_cdata(elem.tail)) + +HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr", + "img", "input", "isindex", "link", "meta", "param") + +try: + HTML_EMPTY = set(HTML_EMPTY) +except NameError: + pass + +def _serialize_html(write, elem, qnames, namespaces, **kwargs): + tag = elem.tag + text = elem.text + if tag is Comment: + write("" % _escape_cdata(text)) + elif tag is ProcessingInstruction: + write("" % _escape_cdata(text)) + else: + tag = qnames[tag] + if tag is None: + if text: + write(_escape_cdata(text)) + for e in elem: + _serialize_html(write, e, qnames, None) + else: + write("<" + tag) + items = list(elem.items()) + if items or namespaces: + if namespaces: + for v, k in sorted(namespaces.items(), + key=lambda x: x[1]): # sort on prefix + if k: + k = ":" + k + write(" xmlns%s=\"%s\"" % ( + k, + _escape_attrib(v) + )) + for k, v in sorted(items): # lexical order + if isinstance(k, QName): + k = k.text + if isinstance(v, QName): + v = qnames[v.text] + else: + v = _escape_attrib_html(v) + # FIXME: handle boolean attributes + write(" %s=\"%s\"" % (qnames[k], v)) + write(">") + ltag = tag.lower() + if text: + if ltag == "script" or ltag == "style": + write(text) + else: + write(_escape_cdata(text)) + for e in elem: + _serialize_html(write, e, qnames, None) + if ltag not in HTML_EMPTY: + write("") + if elem.tail: + write(_escape_cdata(elem.tail)) + +def _serialize_text(write, elem): + for part in elem.itertext(): + write(part) + if elem.tail: + write(elem.tail) + +_serialize = { + "xml": _serialize_xml, + "html": _serialize_html, + "text": _serialize_text, +# this optional method is imported at the end of the module +# "c14n": _serialize_c14n, +} + + +def register_namespace(prefix, uri): + """Register a namespace prefix. + + The registry is global, and any existing mapping for either the + given prefix or the namespace URI will be removed. + + *prefix* is the namespace prefix, *uri* is a namespace uri. Tags and + attributes in this namespace will be serialized with prefix if possible. + + ValueError is raised if prefix is reserved or is invalid. + + """ + if re.match(r"ns\d+$", prefix): + raise ValueError("Prefix format reserved for internal use") + for k, v in list(_namespace_map.items()): + if k == uri or v == prefix: + del _namespace_map[k] + _namespace_map[uri] = prefix + +_namespace_map = { + # "well-known" namespace prefixes + "http://www.w3.org/XML/1998/namespace": "xml", + "http://www.w3.org/1999/xhtml": "html", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", + "http://schemas.xmlsoap.org/wsdl/": "wsdl", + # xml schema + "http://www.w3.org/2001/XMLSchema": "xs", + "http://www.w3.org/2001/XMLSchema-instance": "xsi", + # dublin core + "http://purl.org/dc/elements/1.1/": "dc", +} +# For tests and troubleshooting +register_namespace._namespace_map = _namespace_map + +def _raise_serialization_error(text): + raise TypeError( + "cannot serialize %r (type %s)" % (text, type(text).__name__) + ) + +def _escape_cdata(text): + # escape character data + try: + # it's worth avoiding do-nothing calls for strings that are + # shorter than 500 characters, or so. assume that's, by far, + # the most common case in most applications. + if "&" in text: + text = text.replace("&", "&") + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + return text + except (TypeError, AttributeError): + _raise_serialization_error(text) + +def _escape_attrib(text): + # escape attribute value + try: + if "&" in text: + text = text.replace("&", "&") + if "<" in text: + text = text.replace("<", "<") + if ">" in text: + text = text.replace(">", ">") + if "\"" in text: + text = text.replace("\"", """) + # The following business with carriage returns is to satisfy + # Section 2.11 of the XML specification, stating that + # CR or CR LN should be replaced with just LN + # http://www.w3.org/TR/REC-xml/#sec-line-ends + if "\r\n" in text: + text = text.replace("\r\n", "\n") + if "\r" in text: + text = text.replace("\r", "\n") + #The following four lines are issue 17582 + if "\n" in text: + text = text.replace("\n", " ") + if "\t" in text: + text = text.replace("\t", " ") + return text + except (TypeError, AttributeError): + _raise_serialization_error(text) + +def _escape_attrib_html(text): + # escape attribute value + try: + if "&" in text: + text = text.replace("&", "&") + if ">" in text: + text = text.replace(">", ">") + if "\"" in text: + text = text.replace("\"", """) + return text + except (TypeError, AttributeError): + _raise_serialization_error(text) + +# -------------------------------------------------------------------- + +def tostring(element, encoding=None, method=None, *, + short_empty_elements=True): + """Generate string representation of XML element. + + All subelements are included. If encoding is "unicode", a string + is returned. Otherwise a bytestring is returned. + + *element* is an Element instance, *encoding* is an optional output + encoding defaulting to US-ASCII, *method* is an optional output which can + be one of "xml" (default), "html", "text" or "c14n". + + Returns an (optionally) encoded string containing the XML data. + + """ + stream = io.StringIO() if encoding == 'unicode' else io.BytesIO() + ElementTree(element).write(stream, encoding, method=method, + short_empty_elements=short_empty_elements) + return stream.getvalue() + +class _ListDataStream(io.BufferedIOBase): + """An auxiliary stream accumulating into a list reference.""" + def __init__(self, lst): + self.lst = lst + + def writable(self): + return True + + def seekable(self): + return True + + def write(self, b): + self.lst.append(b) + + def tell(self): + return len(self.lst) + +def tostringlist(element, encoding=None, method=None, *, + short_empty_elements=True): + lst = [] + stream = _ListDataStream(lst) + ElementTree(element).write(stream, encoding, method=method, + short_empty_elements=short_empty_elements) + return lst + + +def dump(elem): + """Write element tree or element structure to sys.stdout. + + This function should be used for debugging only. + + *elem* is either an ElementTree, or a single Element. The exact output + format is implementation dependent. In this version, it's written as an + ordinary XML file. + + """ + # debugging + if not isinstance(elem, ElementTree): + elem = ElementTree(elem) + elem.write(sys.stdout, encoding="unicode") + tail = elem.getroot().tail + if not tail or tail[-1] != "\n": + sys.stdout.write("\n") + +# -------------------------------------------------------------------- +# parsing + + +def parse(source, parser=None): + """Parse XML document into element tree. + + *source* is a filename or file object containing XML data, + *parser* is an optional parser instance defaulting to XMLParser. + + Return an ElementTree instance. + + """ + tree = ElementTree() + tree.parse(source, parser) + return tree + + +def iterparse(source, events=None, parser=None): + """Incrementally parse XML document into ElementTree. + + This class also reports what's going on to the user based on the + *events* it is initialized with. The supported events are the strings + "start", "end", "start-ns" and "end-ns" (the "ns" events are used to get + detailed namespace information). If *events* is omitted, only + "end" events are reported. + + *source* is a filename or file object containing XML data, *events* is + a list of events to report back, *parser* is an optional parser instance. + + Returns an iterator providing (event, elem) pairs. + + """ + # Use the internal, undocumented _parser argument for now; When the + # parser argument of iterparse is removed, this can be killed. + pullparser = XMLPullParser(events=events, _parser=parser) + def iterator(): + try: + while True: + yield from pullparser.read_events() + # load event buffer + data = source.read(16 * 1024) + if not data: + break + pullparser.feed(data) + root = pullparser._close_and_return_root() + yield from pullparser.read_events() + it.root = root + finally: + if close_source: + source.close() + + class IterParseIterator(collections.abc.Iterator): + __next__ = iterator().__next__ + it = IterParseIterator() + it.root = None + del iterator, IterParseIterator + + close_source = False + if not hasattr(source, "read"): + source = open(source, "rb") + close_source = True + + return it + + +class XMLPullParser: + + def __init__(self, events=None, *, _parser=None): + # The _parser argument is for internal use only and must not be relied + # upon in user code. It will be removed in a future release. + # See http://bugs.python.org/issue17741 for more details. + + self._events_queue = collections.deque() + self._parser = _parser or XMLParser(target=TreeBuilder()) + # wire up the parser for event reporting + if events is None: + events = ("end",) + self._parser._setevents(self._events_queue, events) + + def feed(self, data): + """Feed encoded data to parser.""" + if self._parser is None: + raise ValueError("feed() called after end of stream") + if data: + try: + self._parser.feed(data) + except SyntaxError as exc: + self._events_queue.append(exc) + + def _close_and_return_root(self): + # iterparse needs this to set its root attribute properly :( + root = self._parser.close() + self._parser = None + return root + + def close(self): + """Finish feeding data to parser. + + Unlike XMLParser, does not return the root element. Use + read_events() to consume elements from XMLPullParser. + """ + self._close_and_return_root() + + def read_events(self): + """Return an iterator over currently available (event, elem) pairs. + + Events are consumed from the internal event queue as they are + retrieved from the iterator. + """ + events = self._events_queue + while events: + event = events.popleft() + if isinstance(event, Exception): + raise event + else: + yield event + + +def XML(text, parser=None): + """Parse XML document from string constant. + + This function can be used to embed "XML Literals" in Python code. + + *text* is a string containing XML data, *parser* is an + optional parser instance, defaulting to the standard XMLParser. + + Returns an Element instance. + + """ + if not parser: + parser = XMLParser(target=TreeBuilder()) + parser.feed(text) + return parser.close() + + +def XMLID(text, parser=None): + """Parse XML document from string constant for its IDs. + + *text* is a string containing XML data, *parser* is an + optional parser instance, defaulting to the standard XMLParser. + + Returns an (Element, dict) tuple, in which the + dict maps element id:s to elements. + + """ + if not parser: + parser = XMLParser(target=TreeBuilder()) + parser.feed(text) + tree = parser.close() + ids = {} + for elem in tree.iter(): + id = elem.get("id") + if id: + ids[id] = elem + return tree, ids + +# Parse XML document from string constant. Alias for XML(). +fromstring = XML + +def fromstringlist(sequence, parser=None): + """Parse XML document from sequence of string fragments. + + *sequence* is a list of other sequence, *parser* is an optional parser + instance, defaulting to the standard XMLParser. + + Returns an Element instance. + + """ + if not parser: + parser = XMLParser(target=TreeBuilder()) + for text in sequence: + parser.feed(text) + return parser.close() + +# -------------------------------------------------------------------- + + +class TreeBuilder: + """Generic element structure builder. + + This builder converts a sequence of start, data, and end method + calls to a well-formed element structure. + + You can use this class to build an element structure using a custom XML + parser, or a parser for some other XML-like format. + + *element_factory* is an optional element factory which is called + to create new Element instances, as necessary. + + """ + def __init__(self, element_factory=None): + self._data = [] # data collector + self._elem = [] # element stack + self._last = None # last element + self._tail = None # true if we're after an end tag + if element_factory is None: + element_factory = Element + self._factory = element_factory + + def close(self): + """Flush builder buffers and return toplevel document Element.""" + assert len(self._elem) == 0, "missing end tags" + assert self._last is not None, "missing toplevel element" + return self._last + + def _flush(self): + if self._data: + if self._last is not None: + text = "".join(self._data) + if self._tail: + assert self._last.tail is None, "internal error (tail)" + self._last.tail = text + else: + assert self._last.text is None, "internal error (text)" + self._last.text = text + self._data = [] + + def data(self, data): + """Add text to current element.""" + self._data.append(data) + + def start(self, tag, attrs): + """Open new element and return it. + + *tag* is the element name, *attrs* is a dict containing element + attributes. + + """ + self._flush() + self._last = elem = self._factory(tag, attrs) + if self._elem: + self._elem[-1].append(elem) + self._elem.append(elem) + self._tail = 0 + return elem + + def end(self, tag): + """Close and return current Element. + + *tag* is the element name. + + """ + self._flush() + self._last = self._elem.pop() + assert self._last.tag == tag,\ + "end tag mismatch (expected %s, got %s)" % ( + self._last.tag, tag) + self._tail = 1 + return self._last + +_sentinel = ['sentinel'] + +# also see ElementTree and TreeBuilder +class XMLParser: + """Element structure builder for XML source data based on the expat parser. + + *html* are predefined HTML entities (deprecated and not supported), + *target* is an optional target object which defaults to an instance of the + standard TreeBuilder class, *encoding* is an optional encoding string + which if given, overrides the encoding specified in the XML file: + http://www.iana.org/assignments/character-sets + + """ + + def __init__(self, html=_sentinel, target=None, encoding=None): + if html is not _sentinel: + warnings.warn( + "The html argument of XMLParser() is deprecated", + DeprecationWarning, stacklevel=2) + try: + from xml.parsers import expat + except ImportError: + try: + import pyexpat as expat + except ImportError: + raise ImportError( + "No module named expat; use SimpleXMLTreeBuilder instead" + ) + parser = expat.ParserCreate(encoding, "}") + if target is None: + target = TreeBuilder() + # underscored names are provided for compatibility only + self.parser = self._parser = parser + self.target = self._target = target + self._error = expat.error + self._names = {} # name memo cache + # main callbacks + parser.DefaultHandlerExpand = self._default + if hasattr(target, 'start'): + parser.StartElementHandler = self._start + if hasattr(target, 'end'): + parser.EndElementHandler = self._end + if hasattr(target, 'data'): + parser.CharacterDataHandler = target.data + # miscellaneous callbacks + if hasattr(target, 'comment'): + parser.CommentHandler = target.comment + if hasattr(target, 'pi'): + parser.ProcessingInstructionHandler = target.pi + # Configure pyexpat: buffering, new-style attribute handling. + parser.buffer_text = 1 + parser.ordered_attributes = 1 + parser.specified_attributes = 1 + self._doctype = None + self.entity = {} + try: + self.version = "Expat %d.%d.%d" % expat.version_info + except AttributeError: + pass # unknown + + def _setevents(self, events_queue, events_to_report): + # Internal API for XMLPullParser + # events_to_report: a list of events to report during parsing (same as + # the *events* of XMLPullParser's constructor. + # events_queue: a list of actual parsing events that will be populated + # by the underlying parser. + # + parser = self._parser + append = events_queue.append + for event_name in events_to_report: + if event_name == "start": + parser.ordered_attributes = 1 + parser.specified_attributes = 1 + def handler(tag, attrib_in, event=event_name, append=append, + start=self._start): + append((event, start(tag, attrib_in))) + parser.StartElementHandler = handler + elif event_name == "end": + def handler(tag, event=event_name, append=append, + end=self._end): + append((event, end(tag))) + parser.EndElementHandler = handler + elif event_name == "start-ns": + def handler(prefix, uri, event=event_name, append=append): + append((event, (prefix or "", uri or ""))) + parser.StartNamespaceDeclHandler = handler + elif event_name == "end-ns": + def handler(prefix, event=event_name, append=append): + append((event, None)) + parser.EndNamespaceDeclHandler = handler + else: + raise ValueError("unknown event %r" % event_name) + + def _raiseerror(self, value): + err = ParseError(value) + err.code = value.code + err.position = value.lineno, value.offset + raise err + + def _fixname(self, key): + # expand qname, and convert name string to ascii, if possible + try: + name = self._names[key] + except KeyError: + name = key + if "}" in name: + name = "{" + name + self._names[key] = name + return name + + def _start(self, tag, attr_list): + # Handler for expat's StartElementHandler. Since ordered_attributes + # is set, the attributes are reported as a list of alternating + # attribute name,value. + fixname = self._fixname + tag = fixname(tag) + attrib = {} + if attr_list: + for i in range(0, len(attr_list), 2): + attrib[fixname(attr_list[i])] = attr_list[i+1] + return self.target.start(tag, attrib) + + def _end(self, tag): + return self.target.end(self._fixname(tag)) + + def _default(self, text): + prefix = text[:1] + if prefix == "&": + # deal with undefined entities + try: + data_handler = self.target.data + except AttributeError: + return + try: + data_handler(self.entity[text[1:-1]]) + except KeyError: + from xml.parsers import expat + err = expat.error( + "undefined entity %s: line %d, column %d" % + (text, self.parser.ErrorLineNumber, + self.parser.ErrorColumnNumber) + ) + err.code = 11 # XML_ERROR_UNDEFINED_ENTITY + err.lineno = self.parser.ErrorLineNumber + err.offset = self.parser.ErrorColumnNumber + raise err + elif prefix == "<" and text[:9] == "": + self._doctype = None + return + text = text.strip() + if not text: + return + self._doctype.append(text) + n = len(self._doctype) + if n > 2: + type = self._doctype[1] + if type == "PUBLIC" and n == 4: + name, type, pubid, system = self._doctype + if pubid: + pubid = pubid[1:-1] + elif type == "SYSTEM" and n == 3: + name, type, system = self._doctype + pubid = None + else: + return + if hasattr(self.target, "doctype"): + self.target.doctype(name, pubid, system[1:-1]) + elif self.doctype != self._XMLParser__doctype: + # warn about deprecated call + self._XMLParser__doctype(name, pubid, system[1:-1]) + self.doctype(name, pubid, system[1:-1]) + self._doctype = None + + def doctype(self, name, pubid, system): + """(Deprecated) Handle doctype declaration + + *name* is the Doctype name, *pubid* is the public identifier, + and *system* is the system identifier. + + """ + warnings.warn( + "This method of XMLParser is deprecated. Define doctype() " + "method on the TreeBuilder target.", + DeprecationWarning, + ) + + # sentinel, if doctype is redefined in a subclass + __doctype = doctype + + def feed(self, data): + """Feed encoded data to parser.""" + try: + self.parser.Parse(data, 0) + except self._error as v: + self._raiseerror(v) + + def close(self): + """Finish feeding data to parser and return element structure.""" + try: + self.parser.Parse("", 1) # end of data + except self._error as v: + self._raiseerror(v) + try: + close_handler = self.target.close + except AttributeError: + pass + else: + return close_handler() + finally: + # get rid of circular references + del self.parser, self._parser + del self.target, self._target + + +# Import the C accelerators +try: + # Element is going to be shadowed by the C implementation. We need to keep + # the Python version of it accessible for some "creative" by external code + # (see tests) + _Element_Py = Element + + # Element, SubElement, ParseError, TreeBuilder, XMLParser + from _elementtree import * +except ImportError: + pass diff --git a/addon/xml2/etree/__init__.py b/addon/globalPlugins/readFeeds/xml/etree/__init__.py similarity index 100% rename from addon/xml2/etree/__init__.py rename to addon/globalPlugins/readFeeds/xml/etree/__init__.py diff --git a/addon/globalPlugins/readFeeds/xml/etree/cElementTree.py b/addon/globalPlugins/readFeeds/xml/etree/cElementTree.py new file mode 100644 index 00000000..368e6791 --- /dev/null +++ b/addon/globalPlugins/readFeeds/xml/etree/cElementTree.py @@ -0,0 +1,3 @@ +# Deprecated alias for xml.etree.ElementTree + +from xml.etree.ElementTree import * diff --git a/addon/xml2/parsers/__init__.py b/addon/globalPlugins/readFeeds/xml/parsers/__init__.py similarity index 100% rename from addon/xml2/parsers/__init__.py rename to addon/globalPlugins/readFeeds/xml/parsers/__init__.py diff --git a/addon/globalPlugins/readFeeds/xml/parsers/expat.py b/addon/globalPlugins/readFeeds/xml/parsers/expat.py new file mode 100644 index 00000000..bcbe9fb1 --- /dev/null +++ b/addon/globalPlugins/readFeeds/xml/parsers/expat.py @@ -0,0 +1,8 @@ +"""Interface to the Expat non-validating XML parser.""" +import sys + +from pyexpat import * + +# provide pyexpat submodules as xml.parsers.expat submodules +sys.modules['xml.parsers.expat.model'] = model +sys.modules['xml.parsers.expat.errors'] = errors diff --git a/addon/xml2/sax/__init__.py b/addon/globalPlugins/readFeeds/xml/sax/__init__.py similarity index 88% rename from addon/xml2/sax/__init__.py rename to addon/globalPlugins/readFeeds/xml/sax/__init__.py index 005b66e3..13f6cf58 100644 --- a/addon/xml2/sax/__init__.py +++ b/addon/globalPlugins/readFeeds/xml/sax/__init__.py @@ -19,9 +19,9 @@ expatreader -- Driver that allows use of the Expat parser with SAX. """ -from xmlreader import InputSource -from handler import ContentHandler, ErrorHandler -from _exceptions import SAXException, SAXNotRecognizedException, \ +from .xmlreader import InputSource +from .handler import ContentHandler, ErrorHandler +from ._exceptions import SAXException, SAXNotRecognizedException, \ SAXParseException, SAXNotSupportedException, \ SAXReaderNotAvailable @@ -33,11 +33,7 @@ def parse(source, handler, errorHandler=ErrorHandler()): parser.parse(source) def parseString(string, handler, errorHandler=ErrorHandler()): - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO - + import io if errorHandler is None: errorHandler = ErrorHandler() parser = make_parser() @@ -45,7 +41,10 @@ def parseString(string, handler, errorHandler=ErrorHandler()): parser.setErrorHandler(errorHandler) inpsrc = InputSource() - inpsrc.setByteStream(StringIO(string)) + if isinstance(string, str): + inpsrc.setCharacterStream(io.StringIO(string)) + else: + inpsrc.setByteStream(io.BytesIO(string)) parser.parse(inpsrc) # this is the parser list used by the make_parser function if no @@ -59,7 +58,7 @@ def parseString(string, handler, errorHandler=ErrorHandler()): import xml.sax.expatreader import os, sys -if "PY_SAX_PARSER" in os.environ: +if not sys.flags.ignore_environment and "PY_SAX_PARSER" in os.environ: default_parser_list = os.environ["PY_SAX_PARSER"].split(",") del os @@ -79,7 +78,7 @@ def make_parser(parser_list = []): for parser_name in parser_list + default_parser_list: try: return _create_parser(parser_name) - except ImportError,e: + except ImportError as e: import sys if parser_name in sys.modules: # The parser module was found, but importing it diff --git a/addon/xml2/sax/_exceptions.py b/addon/globalPlugins/readFeeds/xml/sax/_exceptions.py similarity index 96% rename from addon/xml2/sax/_exceptions.py rename to addon/globalPlugins/readFeeds/xml/sax/_exceptions.py index fdd614ae..a9b2ba35 100644 --- a/addon/xml2/sax/_exceptions.py +++ b/addon/globalPlugins/readFeeds/xml/sax/_exceptions.py @@ -12,7 +12,7 @@ class SAXException(Exception): the application: you can subclass it to provide additional functionality, or to add localization. Note that although you will receive a SAXException as the argument to the handlers in the - ErrorHandler interface, you are not actually required to throw + ErrorHandler interface, you are not actually required to raise the exception; instead, you can simply read the information in it.""" @@ -50,7 +50,7 @@ class SAXParseException(SAXException): the original XML document. Note that although the application will receive a SAXParseException as the argument to the handlers in the ErrorHandler interface, the application is not actually required - to throw the exception; instead, it can simply read the + to raise the exception; instead, it can simply read the information in it and take a different action. Since this exception is a subclass of SAXException, it inherits @@ -62,7 +62,7 @@ def __init__(self, msg, exception, locator): self._locator = locator # We need to cache this stuff at construction time. - # If this exception is thrown, the objects through which we must + # If this exception is raised, the objects through which we must # traverse to get this information may be deleted by the time # it gets caught. self._systemId = self._locator.getSystemId() diff --git a/addon/xml2/sax/expatreader.py b/addon/globalPlugins/readFeeds/xml/sax/expatreader.py similarity index 89% rename from addon/xml2/sax/expatreader.py rename to addon/globalPlugins/readFeeds/xml/sax/expatreader.py index 92a79c1c..5066ffc2 100644 --- a/addon/xml2/sax/expatreader.py +++ b/addon/globalPlugins/readFeeds/xml/sax/expatreader.py @@ -43,6 +43,9 @@ def _mkproxy(o): _mkproxy = weakref.proxy del weakref, _weakref +class _ClosedParser: + pass + # --- ExpatLocator class ExpatLocator(xmlreader.Locator): @@ -92,7 +95,7 @@ def __init__(self, namespaceHandling=0, bufsize=2**16-20): self._lex_handler_prop = None self._parsing = 0 self._entity_stack = [] - self._external_ges = 1 + self._external_ges = 0 self._interning = None # XMLReader methods @@ -102,9 +105,16 @@ def parse(self, source): source = saxutils.prepare_input_source(source) self._source = source - self.reset() - self._cont_handler.setDocumentLocator(ExpatLocator(self)) - xmlreader.IncrementalParser.parse(self, source) + try: + self.reset() + self._cont_handler.setDocumentLocator(ExpatLocator(self)) + xmlreader.IncrementalParser.parse(self, source) + except: + # bpo-30264: Close the source on error to not leak resources: + # xml.sax.parse() doesn't give access to the underlying parser + # to the caller + self._close_source() + raise def prepareParser(self, source): if source.getSystemId() is not None: @@ -205,20 +215,42 @@ def feed(self, data, isFinal = 0): # document. When feeding chunks, they are not normally final - # except when invoked from close. self._parser.Parse(data, isFinal) - except expat.error, e: + except expat.error as e: exc = SAXParseException(expat.ErrorString(e.code), e, self) # FIXME: when to invoke error()? self._err_handler.fatalError(exc) + def _close_source(self): + source = self._source + try: + file = source.getCharacterStream() + if file is not None: + file.close() + finally: + file = source.getByteStream() + if file is not None: + file.close() + def close(self): - if self._entity_stack: + if (self._entity_stack or self._parser is None or + isinstance(self._parser, _ClosedParser)): # If we are completing an external entity, do nothing here return - self.feed("", isFinal = 1) - self._cont_handler.endDocument() - self._parsing = 0 - # break cycle created by expat handlers pointing to our methods - self._parser = None + try: + self.feed("", isFinal = 1) + self._cont_handler.endDocument() + self._parsing = 0 + # break cycle created by expat handlers pointing to our methods + self._parser = None + finally: + self._parsing = 0 + if self._parser is not None: + # Keep ErrorColumnNumber and ErrorLineNumber after closing. + parser = _ClosedParser() + parser.ErrorColumnNumber = self._parser.ErrorColumnNumber + parser.ErrorLineNumber = self._parser.ErrorLineNumber + self._parser = parser + self._close_source() def _reset_cont_handler(self): self._parser.ProcessingInstructionHandler = \ diff --git a/addon/xml2/sax/handler.py b/addon/globalPlugins/readFeeds/xml/sax/handler.py similarity index 99% rename from addon/xml2/sax/handler.py rename to addon/globalPlugins/readFeeds/xml/sax/handler.py index f9e91b6d..481733d2 100644 --- a/addon/xml2/sax/handler.py +++ b/addon/globalPlugins/readFeeds/xml/sax/handler.py @@ -39,7 +39,7 @@ def fatalError(self, exception): def warning(self, exception): "Handle a warning." - print exception + print(exception) # ===== CONTENTHANDLER ===== diff --git a/addon/xml2/sax/saxutils.py b/addon/globalPlugins/readFeeds/xml/sax/saxutils.py similarity index 69% rename from addon/xml2/sax/saxutils.py rename to addon/globalPlugins/readFeeds/xml/sax/saxutils.py index 97d65d8f..a69c7f76 100644 --- a/addon/xml2/sax/saxutils.py +++ b/addon/globalPlugins/readFeeds/xml/sax/saxutils.py @@ -3,23 +3,11 @@ convenience of application and driver writers. """ -import os, urlparse, urllib, types -import handler -import xmlreader - -try: - _StringTypes = [types.StringType, types.UnicodeType] -except AttributeError: - _StringTypes = [types.StringType] - -# See whether the xmlcharrefreplace error handler is -# supported -try: - from codecs import xmlcharrefreplace_errors - _error_handling = "xmlcharrefreplace" - del xmlcharrefreplace_errors -except ImportError: - _error_handling = "strict" +import os, urllib.parse, urllib.request +import io +import codecs +from . import handler +from . import xmlreader def __dict_replace(s, d): """Replace substrings of a string using a dictionary.""" @@ -81,24 +69,60 @@ def quoteattr(data, entities={}): return data +def _gettextwriter(out, encoding): + if out is None: + import sys + return sys.stdout + + if isinstance(out, io.TextIOBase): + # use a text writer as is + return out + + if isinstance(out, (codecs.StreamWriter, codecs.StreamReaderWriter)): + # use a codecs stream writer as is + return out + + # wrap a binary writer with TextIOWrapper + if isinstance(out, io.RawIOBase): + # Keep the original file open when the TextIOWrapper is + # destroyed + class _wrapper: + __class__ = out.__class__ + def __getattr__(self, name): + return getattr(out, name) + buffer = _wrapper() + buffer.close = lambda: None + else: + # This is to handle passed objects that aren't in the + # IOBase hierarchy, but just have a write method + buffer = io.BufferedIOBase() + buffer.writable = lambda: True + buffer.write = out.write + try: + # TextIOWrapper uses this methods to determine + # if BOM (for UTF-16, etc) should be added + buffer.seekable = out.seekable + buffer.tell = out.tell + except AttributeError: + pass + return io.TextIOWrapper(buffer, encoding=encoding, + errors='xmlcharrefreplace', + newline='\n', + write_through=True) + class XMLGenerator(handler.ContentHandler): - def __init__(self, out=None, encoding="iso-8859-1"): - if out is None: - import sys - out = sys.stdout + def __init__(self, out=None, encoding="iso-8859-1", short_empty_elements=False): handler.ContentHandler.__init__(self) - self._out = out + out = _gettextwriter(out, encoding) + self._write = out.write + self._flush = out.flush self._ns_contexts = [{}] # contains uri -> prefix dicts self._current_context = self._ns_contexts[-1] self._undeclared_ns_maps = [] self._encoding = encoding - - def _write(self, text): - if isinstance(text, str): - self._out.write(text) - else: - self._out.write(text.encode(self._encoding, _error_handling)) + self._short_empty_elements = short_empty_elements + self._pending_start_element = False def _qname(self, name): """Builds a qualified name from a (ns_url, localname) pair""" @@ -117,12 +141,20 @@ def _qname(self, name): # Return the unqualified name return name[1] + def _finish_pending_start_element(self,endElement=False): + if self._pending_start_element: + self._write('>') + self._pending_start_element = False + # ContentHandler methods def startDocument(self): self._write('\n' % self._encoding) + def endDocument(self): + self._flush() + def startPrefixMapping(self, prefix, uri): self._ns_contexts.append(self._current_context.copy()) self._current_context[uri] = prefix @@ -133,38 +165,63 @@ def endPrefixMapping(self, prefix): del self._ns_contexts[-1] def startElement(self, name, attrs): + self._finish_pending_start_element() self._write('<' + name) for (name, value) in attrs.items(): self._write(' %s=%s' % (name, quoteattr(value))) - self._write('>') + if self._short_empty_elements: + self._pending_start_element = True + else: + self._write(">") def endElement(self, name): - self._write('' % name) + if self._pending_start_element: + self._write('/>') + self._pending_start_element = False + else: + self._write('' % name) def startElementNS(self, name, qname, attrs): + self._finish_pending_start_element() self._write('<' + self._qname(name)) for prefix, uri in self._undeclared_ns_maps: if prefix: - self._out.write(' xmlns:%s="%s"' % (prefix, uri)) + self._write(' xmlns:%s="%s"' % (prefix, uri)) else: - self._out.write(' xmlns="%s"' % uri) + self._write(' xmlns="%s"' % uri) self._undeclared_ns_maps = [] for (name, value) in attrs.items(): self._write(' %s=%s' % (self._qname(name), quoteattr(value))) - self._write('>') + if self._short_empty_elements: + self._pending_start_element = True + else: + self._write(">") def endElementNS(self, name, qname): - self._write('' % self._qname(name)) + if self._pending_start_element: + self._write('/>') + self._pending_start_element = False + else: + self._write('' % self._qname(name)) def characters(self, content): - self._write(escape(content)) + if content: + self._finish_pending_start_element() + if not isinstance(content, str): + content = str(content, self._encoding) + self._write(escape(content)) def ignorableWhitespace(self, content): - self._write(content) + if content: + self._finish_pending_start_element() + if not isinstance(content, str): + content = str(content, self._encoding) + self._write(content) def processingInstruction(self, target, data): + self._finish_pending_start_element() self._write('' % (target, data)) @@ -279,20 +336,23 @@ def setParent(self, parent): # --- Utility functions -def prepare_input_source(source, base = ""): +def prepare_input_source(source, base=""): """This function takes an InputSource and an optional base URL and returns a fully resolved InputSource object ready for reading.""" - if type(source) in _StringTypes: + if isinstance(source, str): source = xmlreader.InputSource(source) elif hasattr(source, "read"): f = source source = xmlreader.InputSource() - source.setByteStream(f) - if hasattr(f, "name"): + if isinstance(f.read(0), str): + source.setCharacterStream(f) + else: + source.setByteStream(f) + if hasattr(f, "name") and isinstance(f.name, str): source.setSystemId(f.name) - if source.getByteStream() is None: + if source.getCharacterStream() is None and source.getByteStream() is None: sysid = source.getSystemId() basehead = os.path.dirname(os.path.normpath(base)) sysidfilename = os.path.join(basehead, sysid) @@ -300,8 +360,8 @@ def prepare_input_source(source, base = ""): source.setSystemId(sysidfilename) f = open(sysidfilename, "rb") else: - source.setSystemId(urlparse.urljoin(base, sysid)) - f = urllib.urlopen(source.getSystemId()) + source.setSystemId(urllib.parse.urljoin(base, sysid)) + f = urllib.request.urlopen(source.getSystemId()) source.setByteStream(f) diff --git a/addon/xml2/sax/xmlreader.py b/addon/globalPlugins/readFeeds/xml/sax/xmlreader.py similarity index 93% rename from addon/xml2/sax/xmlreader.py rename to addon/globalPlugins/readFeeds/xml/sax/xmlreader.py index 46ee02b4..716f2284 100644 --- a/addon/xml2/sax/xmlreader.py +++ b/addon/globalPlugins/readFeeds/xml/sax/xmlreader.py @@ -1,9 +1,9 @@ """An XML Reader is the SAX 2 name for an XML parser. XML Parsers should be based on this code. """ -import handler +from . import handler -from _exceptions import SAXNotSupportedException, SAXNotRecognizedException +from ._exceptions import SAXNotSupportedException, SAXNotRecognizedException # ===== XMLREADER ===== @@ -68,7 +68,7 @@ def setLocale(self, locale): SAX parsers are not required to provide localization for errors and warnings; if they cannot support the requested locale, - however, they must throw a SAX exception. Applications may + however, they must raise a SAX exception. Applications may request a locale change in the middle of a parse.""" raise SAXNotSupportedException("Locale support not implemented") @@ -113,13 +113,15 @@ def __init__(self, bufsize=2**16): XMLReader.__init__(self) def parse(self, source): - import saxutils + from . import saxutils source = saxutils.prepare_input_source(source) self.prepareParser(source) - file = source.getByteStream() + file = source.getCharacterStream() + if file is None: + file = source.getByteStream() buffer = file.read(self._bufsize) - while buffer != "": + while buffer: self.feed(buffer) buffer = file.read(self._bufsize) self.close() @@ -294,20 +296,20 @@ def getValueByQName(self, name): return self._attrs[name] def getNameByQName(self, name): - if not name in self._attrs: - raise KeyError, name + if name not in self._attrs: + raise KeyError(name) return name def getQNameByName(self, name): - if not name in self._attrs: - raise KeyError, name + if name not in self._attrs: + raise KeyError(name) return name def getNames(self): - return self._attrs.keys() + return list(self._attrs.keys()) def getQNames(self): - return self._attrs.keys() + return list(self._attrs.keys()) def __len__(self): return len(self._attrs) @@ -316,10 +318,7 @@ def __getitem__(self, name): return self._attrs[name] def keys(self): - return self._attrs.keys() - - def has_key(self, name): - return name in self._attrs + return list(self._attrs.keys()) def __contains__(self, name): return name in self._attrs @@ -331,10 +330,10 @@ def copy(self): return self.__class__(self._attrs) def items(self): - return self._attrs.items() + return list(self._attrs.items()) def values(self): - return self._attrs.values() + return list(self._attrs.values()) # ===== ATTRIBUTESNSIMPL ===== @@ -353,20 +352,20 @@ def getValueByQName(self, name): if qname == name: return self._attrs[nsname] - raise KeyError, name + raise KeyError(name) def getNameByQName(self, name): for (nsname, qname) in self._qnames.items(): if qname == name: return nsname - raise KeyError, name + raise KeyError(name) def getQNameByName(self, name): return self._qnames[name] def getQNames(self): - return self._qnames.values() + return list(self._qnames.values()) def copy(self): return self.__class__(self._attrs, self._qnames) diff --git a/addon/installTasks.py b/addon/installTasks.py index 5488cda5..e0ece84b 100644 --- a/addon/installTasks.py +++ b/addon/installTasks.py @@ -12,23 +12,13 @@ import gui import wx -ADDON_DIR = os.path.dirname(__file__).decode("mbcs") +ADDON_DIR = os.path.abspath(os.path.dirname(__file__)) FEEDS_PATH = os.path.join(ADDON_DIR, "globalPlugins", "readFeeds", "personalFeeds") CONFIG_PATH = globalVars.appArgs.configPath addonHandler.initTranslation() def onInstall(): - for addon in addonHandler.getAvailableAddons(): - if addon.manifest['name'] == "ReadFeeds": - if gui.messageBox( - # Translators: the label of a message box dialog. - _("You have installed the ReadFeeds add-on, probably an old and incompatible version with this one. Do you want to uninstall the old version?"), - # Translators: the title of a message box dialog. - _("Uninstall incompatible add-on"), - wx.YES|wx.NO|wx.ICON_WARNING) == wx.YES: - addon.requestRemove() - break addonPath = [os.path.join(CONFIG_PATH, "RSS"), os.path.join(CONFIG_PATH, "personalFeeds")] for path in addonPath: if not os.path.isdir(path): diff --git a/addon/xml2/__init__.pyo b/addon/xml2/__init__.pyo deleted file mode 100644 index 885271dc..00000000 Binary files a/addon/xml2/__init__.pyo and /dev/null differ diff --git a/addon/xml2/dom/NodeFilter.pyo b/addon/xml2/dom/NodeFilter.pyo deleted file mode 100644 index ec9eb111..00000000 Binary files a/addon/xml2/dom/NodeFilter.pyo and /dev/null differ diff --git a/addon/xml2/dom/__init__.pyo b/addon/xml2/dom/__init__.pyo deleted file mode 100644 index 44d49539..00000000 Binary files a/addon/xml2/dom/__init__.pyo and /dev/null differ diff --git a/addon/xml2/dom/domreg.pyo b/addon/xml2/dom/domreg.pyo deleted file mode 100644 index 64623912..00000000 Binary files a/addon/xml2/dom/domreg.pyo and /dev/null differ diff --git a/addon/xml2/dom/expatbuilder.pyo b/addon/xml2/dom/expatbuilder.pyo deleted file mode 100644 index 840c042c..00000000 Binary files a/addon/xml2/dom/expatbuilder.pyo and /dev/null differ diff --git a/addon/xml2/dom/minicompat.pyo b/addon/xml2/dom/minicompat.pyo deleted file mode 100644 index bee8f317..00000000 Binary files a/addon/xml2/dom/minicompat.pyo and /dev/null differ diff --git a/addon/xml2/dom/minidom.pyo b/addon/xml2/dom/minidom.pyo deleted file mode 100644 index 045b35da..00000000 Binary files a/addon/xml2/dom/minidom.pyo and /dev/null differ diff --git a/addon/xml2/dom/xmlbuilder.pyo b/addon/xml2/dom/xmlbuilder.pyo deleted file mode 100644 index 7391d27c..00000000 Binary files a/addon/xml2/dom/xmlbuilder.pyo and /dev/null differ diff --git a/addon/xml2/etree/ElementTree.py b/addon/xml2/etree/ElementTree.py deleted file mode 100644 index d89fd3be..00000000 --- a/addon/xml2/etree/ElementTree.py +++ /dev/null @@ -1,1649 +0,0 @@ -# -# ElementTree -# $Id: ElementTree.py 3440 2008-07-18 14:45:01Z fredrik $ -# -# light-weight XML support for Python 2.3 and later. -# -# history (since 1.2.6): -# 2005-11-12 fl added tostringlist/fromstringlist helpers -# 2006-07-05 fl merged in selected changes from the 1.3 sandbox -# 2006-07-05 fl removed support for 2.1 and earlier -# 2007-06-21 fl added deprecation/future warnings -# 2007-08-25 fl added doctype hook, added parser version attribute etc -# 2007-08-26 fl added new serializer code (better namespace handling, etc) -# 2007-08-27 fl warn for broken /tag searches on tree level -# 2007-09-02 fl added html/text methods to serializer (experimental) -# 2007-09-05 fl added method argument to tostring/tostringlist -# 2007-09-06 fl improved error handling -# 2007-09-13 fl added itertext, iterfind; assorted cleanups -# 2007-12-15 fl added C14N hooks, copy method (experimental) -# -# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved. -# -# fredrik@pythonware.com -# http://www.pythonware.com -# -# -------------------------------------------------------------------- -# The ElementTree toolkit is -# -# Copyright (c) 1999-2008 by Fredrik Lundh -# -# By obtaining, using, and/or copying this software and/or its -# associated documentation, you agree that you have read, understood, -# and will comply with the following terms and conditions: -# -# Permission to use, copy, modify, and distribute this software and -# its associated documentation for any purpose and without fee is -# hereby granted, provided that the above copyright notice appears in -# all copies, and that both that copyright notice and this permission -# notice appear in supporting documentation, and that the name of -# Secret Labs AB or the author not be used in advertising or publicity -# pertaining to distribution of the software without specific, written -# prior permission. -# -# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD -# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- -# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR -# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY -# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE -# OF THIS SOFTWARE. -# -------------------------------------------------------------------- - -# Licensed to PSF under a Contributor Agreement. -# See http://www.python.org/psf/license for licensing details. - -__all__ = [ - # public symbols - "Comment", - "dump", - "Element", "ElementTree", - "fromstring", "fromstringlist", - "iselement", "iterparse", - "parse", "ParseError", - "PI", "ProcessingInstruction", - "QName", - "SubElement", - "tostring", "tostringlist", - "TreeBuilder", - "VERSION", - "XML", - "XMLParser", "XMLTreeBuilder", - ] - -VERSION = "1.3.0" - -## -# The Element type is a flexible container object, designed to -# store hierarchical data structures in memory. The type can be -# described as a cross between a list and a dictionary. -#

-# Each element has a number of properties associated with it: -#

-# -# To create an element instance, use the {@link #Element} constructor -# or the {@link #SubElement} factory function. -#

-# The {@link #ElementTree} class can be used to wrap an element -# structure, and convert it from and to XML. -## - -import sys -import re -import warnings - - -class _SimpleElementPath(object): - # emulate pre-1.2 find/findtext/findall behaviour - def find(self, element, tag, namespaces=None): - for elem in element: - if elem.tag == tag: - return elem - return None - def findtext(self, element, tag, default=None, namespaces=None): - elem = self.find(element, tag) - if elem is None: - return default - return elem.text or "" - def iterfind(self, element, tag, namespaces=None): - if tag[:3] == ".//": - for elem in element.iter(tag[3:]): - yield elem - for elem in element: - if elem.tag == tag: - yield elem - def findall(self, element, tag, namespaces=None): - return list(self.iterfind(element, tag, namespaces)) - -try: - from . import ElementPath -except ImportError: - ElementPath = _SimpleElementPath() - -## -# Parser error. This is a subclass of SyntaxError. -#

-# In addition to the exception value, an exception instance contains a -# specific exception code in the code attribute, and the line and -# column of the error in the position attribute. - -class ParseError(SyntaxError): - pass - -# -------------------------------------------------------------------- - -## -# Checks if an object appears to be a valid element object. -# -# @param An element instance. -# @return A true value if this is an element object. -# @defreturn flag - -def iselement(element): - # FIXME: not sure about this; might be a better idea to look - # for tag/attrib/text attributes - return isinstance(element, Element) or hasattr(element, "tag") - -## -# Element class. This class defines the Element interface, and -# provides a reference implementation of this interface. -#

-# The element name, attribute names, and attribute values can be -# either ASCII strings (ordinary Python strings containing only 7-bit -# ASCII characters) or Unicode strings. -# -# @param tag The element name. -# @param attrib An optional dictionary, containing element attributes. -# @param **extra Additional attributes, given as keyword arguments. -# @see Element -# @see SubElement -# @see Comment -# @see ProcessingInstruction - -class Element(object): - # text...tail - - ## - # (Attribute) Element tag. - - tag = None - - ## - # (Attribute) Element attribute dictionary. Where possible, use - # {@link #Element.get}, - # {@link #Element.set}, - # {@link #Element.keys}, and - # {@link #Element.items} to access - # element attributes. - - attrib = None - - ## - # (Attribute) Text before first subelement. This is either a - # string or the value None. Note that if there was no text, this - # attribute may be either None or an empty string, depending on - # the parser. - - text = None - - ## - # (Attribute) Text after this element's end tag, but before the - # next sibling element's start tag. This is either a string or - # the value None. Note that if there was no text, this attribute - # may be either None or an empty string, depending on the parser. - - tail = None # text after end tag, if any - - # constructor - - def __init__(self, tag, attrib={}, **extra): - attrib = attrib.copy() - attrib.update(extra) - self.tag = tag - self.attrib = attrib - self._children = [] - - def __repr__(self): - return "" % (repr(self.tag), id(self)) - - ## - # Creates a new element object of the same type as this element. - # - # @param tag Element tag. - # @param attrib Element attributes, given as a dictionary. - # @return A new element instance. - - def makeelement(self, tag, attrib): - return self.__class__(tag, attrib) - - ## - # (Experimental) Copies the current element. This creates a - # shallow copy; subelements will be shared with the original tree. - # - # @return A new element instance. - - def copy(self): - elem = self.makeelement(self.tag, self.attrib) - elem.text = self.text - elem.tail = self.tail - elem[:] = self - return elem - - ## - # Returns the number of subelements. Note that this only counts - # full elements; to check if there's any content in an element, you - # have to check both the length and the text attribute. - # - # @return The number of subelements. - - def __len__(self): - return len(self._children) - - def __nonzero__(self): - warnings.warn( - "The behavior of this method will change in future versions. " - "Use specific 'len(elem)' or 'elem is not None' test instead.", - FutureWarning, stacklevel=2 - ) - return len(self._children) != 0 # emulate old behaviour, for now - - ## - # Returns the given subelement, by index. - # - # @param index What subelement to return. - # @return The given subelement. - # @exception IndexError If the given element does not exist. - - def __getitem__(self, index): - return self._children[index] - - ## - # Replaces the given subelement, by index. - # - # @param index What subelement to replace. - # @param element The new element value. - # @exception IndexError If the given element does not exist. - - def __setitem__(self, index, element): - # if isinstance(index, slice): - # for elt in element: - # assert iselement(elt) - # else: - # assert iselement(element) - self._children[index] = element - - ## - # Deletes the given subelement, by index. - # - # @param index What subelement to delete. - # @exception IndexError If the given element does not exist. - - def __delitem__(self, index): - del self._children[index] - - ## - # Adds a subelement to the end of this element. In document order, - # the new element will appear after the last existing subelement (or - # directly after the text, if it's the first subelement), but before - # the end tag for this element. - # - # @param element The element to add. - - def append(self, element): - # assert iselement(element) - self._children.append(element) - - ## - # Appends subelements from a sequence. - # - # @param elements A sequence object with zero or more elements. - # @since 1.3 - - def extend(self, elements): - # for element in elements: - # assert iselement(element) - self._children.extend(elements) - - ## - # Inserts a subelement at the given position in this element. - # - # @param index Where to insert the new subelement. - - def insert(self, index, element): - # assert iselement(element) - self._children.insert(index, element) - - ## - # Removes a matching subelement. Unlike the find methods, - # this method compares elements based on identity, not on tag - # value or contents. To remove subelements by other means, the - # easiest way is often to use a list comprehension to select what - # elements to keep, and use slice assignment to update the parent - # element. - # - # @param element What element to remove. - # @exception ValueError If a matching element could not be found. - - def remove(self, element): - # assert iselement(element) - self._children.remove(element) - - ## - # (Deprecated) Returns all subelements. The elements are returned - # in document order. - # - # @return A list of subelements. - # @defreturn list of Element instances - - def getchildren(self): - warnings.warn( - "This method will be removed in future versions. " - "Use 'list(elem)' or iteration over elem instead.", - DeprecationWarning, stacklevel=2 - ) - return self._children - - ## - # Finds the first matching subelement, by tag name or path. - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return The first matching element, or None if no element was found. - # @defreturn Element or None - - def find(self, path, namespaces=None): - return ElementPath.find(self, path, namespaces) - - ## - # Finds text for the first matching subelement, by tag name or path. - # - # @param path What element to look for. - # @param default What to return if the element was not found. - # @keyparam namespaces Optional namespace prefix map. - # @return The text content of the first matching element, or the - # default value no element was found. Note that if the element - # is found, but has no text content, this method returns an - # empty string. - # @defreturn string - - def findtext(self, path, default=None, namespaces=None): - return ElementPath.findtext(self, path, default, namespaces) - - ## - # Finds all matching subelements, by tag name or path. - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return A list or other sequence containing all matching elements, - # in document order. - # @defreturn list of Element instances - - def findall(self, path, namespaces=None): - return ElementPath.findall(self, path, namespaces) - - ## - # Finds all matching subelements, by tag name or path. - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return An iterator or sequence containing all matching elements, - # in document order. - # @defreturn a generated sequence of Element instances - - def iterfind(self, path, namespaces=None): - return ElementPath.iterfind(self, path, namespaces) - - ## - # Resets an element. This function removes all subelements, clears - # all attributes, and sets the text and tail attributes - # to None. - - def clear(self): - self.attrib.clear() - self._children = [] - self.text = self.tail = None - - ## - # Gets an element attribute. Equivalent to attrib.get, but - # some implementations may handle this a bit more efficiently. - # - # @param key What attribute to look for. - # @param default What to return if the attribute was not found. - # @return The attribute value, or the default value, if the - # attribute was not found. - # @defreturn string or None - - def get(self, key, default=None): - return self.attrib.get(key, default) - - ## - # Sets an element attribute. Equivalent to attrib[key] = value, - # but some implementations may handle this a bit more efficiently. - # - # @param key What attribute to set. - # @param value The attribute value. - - def set(self, key, value): - self.attrib[key] = value - - ## - # Gets a list of attribute names. The names are returned in an - # arbitrary order (just like for an ordinary Python dictionary). - # Equivalent to attrib.keys(). - # - # @return A list of element attribute names. - # @defreturn list of strings - - def keys(self): - return self.attrib.keys() - - ## - # Gets element attributes, as a sequence. The attributes are - # returned in an arbitrary order. Equivalent to attrib.items(). - # - # @return A list of (name, value) tuples for all attributes. - # @defreturn list of (string, string) tuples - - def items(self): - return self.attrib.items() - - ## - # Creates a tree iterator. The iterator loops over this element - # and all subelements, in document order, and returns all elements - # with a matching tag. - #

- # If the tree structure is modified during iteration, new or removed - # elements may or may not be included. To get a stable set, use the - # list() function on the iterator, and loop over the resulting list. - # - # @param tag What tags to look for (default is to return all elements). - # @return An iterator containing all the matching elements. - # @defreturn iterator - - def iter(self, tag=None): - if tag == "*": - tag = None - if tag is None or self.tag == tag: - yield self - for e in self._children: - for e in e.iter(tag): - yield e - - # compatibility - def getiterator(self, tag=None): - # Change for a DeprecationWarning in 1.4 - warnings.warn( - "This method will be removed in future versions. " - "Use 'elem.iter()' or 'list(elem.iter())' instead.", - PendingDeprecationWarning, stacklevel=2 - ) - return list(self.iter(tag)) - - ## - # Creates a text iterator. The iterator loops over this element - # and all subelements, in document order, and returns all inner - # text. - # - # @return An iterator containing all inner text. - # @defreturn iterator - - def itertext(self): - tag = self.tag - if not isinstance(tag, basestring) and tag is not None: - return - if self.text: - yield self.text - for e in self: - for s in e.itertext(): - yield s - if e.tail: - yield e.tail - -# compatibility -_Element = _ElementInterface = Element - -## -# Subelement factory. This function creates an element instance, and -# appends it to an existing element. -#

-# The element name, attribute names, and attribute values can be -# either 8-bit ASCII strings or Unicode strings. -# -# @param parent The parent element. -# @param tag The subelement name. -# @param attrib An optional dictionary, containing element attributes. -# @param **extra Additional attributes, given as keyword arguments. -# @return An element instance. -# @defreturn Element - -def SubElement(parent, tag, attrib={}, **extra): - attrib = attrib.copy() - attrib.update(extra) - element = parent.makeelement(tag, attrib) - parent.append(element) - return element - -## -# Comment element factory. This factory function creates a special -# element that will be serialized as an XML comment by the standard -# serializer. -#

-# The comment string can be either an 8-bit ASCII string or a Unicode -# string. -# -# @param text A string containing the comment string. -# @return An element instance, representing a comment. -# @defreturn Element - -def Comment(text=None): - element = Element(Comment) - element.text = text - return element - -## -# PI element factory. This factory function creates a special element -# that will be serialized as an XML processing instruction by the standard -# serializer. -# -# @param target A string containing the PI target. -# @param text A string containing the PI contents, if any. -# @return An element instance, representing a PI. -# @defreturn Element - -def ProcessingInstruction(target, text=None): - element = Element(ProcessingInstruction) - element.text = target - if text: - element.text = element.text + " " + text - return element - -PI = ProcessingInstruction - -## -# QName wrapper. This can be used to wrap a QName attribute value, in -# order to get proper namespace handling on output. -# -# @param text A string containing the QName value, in the form {uri}local, -# or, if the tag argument is given, the URI part of a QName. -# @param tag Optional tag. If given, the first argument is interpreted as -# an URI, and this argument is interpreted as a local name. -# @return An opaque object, representing the QName. - -class QName(object): - def __init__(self, text_or_uri, tag=None): - if tag: - text_or_uri = "{%s}%s" % (text_or_uri, tag) - self.text = text_or_uri - def __str__(self): - return self.text - def __hash__(self): - return hash(self.text) - def __cmp__(self, other): - if isinstance(other, QName): - return cmp(self.text, other.text) - return cmp(self.text, other) - -# -------------------------------------------------------------------- - -## -# ElementTree wrapper class. This class represents an entire element -# hierarchy, and adds some extra support for serialization to and from -# standard XML. -# -# @param element Optional root element. -# @keyparam file Optional file handle or file name. If given, the -# tree is initialized with the contents of this XML file. - -class ElementTree(object): - - def __init__(self, element=None, file=None): - # assert element is None or iselement(element) - self._root = element # first node - if file: - self.parse(file) - - ## - # Gets the root element for this tree. - # - # @return An element instance. - # @defreturn Element - - def getroot(self): - return self._root - - ## - # Replaces the root element for this tree. This discards the - # current contents of the tree, and replaces it with the given - # element. Use with care. - # - # @param element An element instance. - - def _setroot(self, element): - # assert iselement(element) - self._root = element - - ## - # Loads an external XML document into this element tree. - # - # @param source A file name or file object. If a file object is - # given, it only has to implement a read(n) method. - # @keyparam parser An optional parser instance. If not given, the - # standard {@link XMLParser} parser is used. - # @return The document root element. - # @defreturn Element - # @exception ParseError If the parser fails to parse the document. - - def parse(self, source, parser=None): - if not hasattr(source, "read"): - source = open(source, "rb") - if not parser: - parser = XMLParser(target=TreeBuilder()) - while 1: - data = source.read(65536) - if not data: - break - parser.feed(data) - self._root = parser.close() - return self._root - - ## - # Creates a tree iterator for the root element. The iterator loops - # over all elements in this tree, in document order. - # - # @param tag What tags to look for (default is to return all elements) - # @return An iterator. - # @defreturn iterator - - def iter(self, tag=None): - # assert self._root is not None - return self._root.iter(tag) - - # compatibility - def getiterator(self, tag=None): - # Change for a DeprecationWarning in 1.4 - warnings.warn( - "This method will be removed in future versions. " - "Use 'tree.iter()' or 'list(tree.iter())' instead.", - PendingDeprecationWarning, stacklevel=2 - ) - return list(self.iter(tag)) - - ## - # Finds the first toplevel element with given tag. - # Same as getroot().find(path). - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return The first matching element, or None if no element was found. - # @defreturn Element or None - - def find(self, path, namespaces=None): - # assert self._root is not None - if path[:1] == "/": - path = "." + path - warnings.warn( - "This search is broken in 1.3 and earlier, and will be " - "fixed in a future version. If you rely on the current " - "behaviour, change it to %r" % path, - FutureWarning, stacklevel=2 - ) - return self._root.find(path, namespaces) - - ## - # Finds the element text for the first toplevel element with given - # tag. Same as getroot().findtext(path). - # - # @param path What toplevel element to look for. - # @param default What to return if the element was not found. - # @keyparam namespaces Optional namespace prefix map. - # @return The text content of the first matching element, or the - # default value no element was found. Note that if the element - # is found, but has no text content, this method returns an - # empty string. - # @defreturn string - - def findtext(self, path, default=None, namespaces=None): - # assert self._root is not None - if path[:1] == "/": - path = "." + path - warnings.warn( - "This search is broken in 1.3 and earlier, and will be " - "fixed in a future version. If you rely on the current " - "behaviour, change it to %r" % path, - FutureWarning, stacklevel=2 - ) - return self._root.findtext(path, default, namespaces) - - ## - # Finds all toplevel elements with the given tag. - # Same as getroot().findall(path). - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return A list or iterator containing all matching elements, - # in document order. - # @defreturn list of Element instances - - def findall(self, path, namespaces=None): - # assert self._root is not None - if path[:1] == "/": - path = "." + path - warnings.warn( - "This search is broken in 1.3 and earlier, and will be " - "fixed in a future version. If you rely on the current " - "behaviour, change it to %r" % path, - FutureWarning, stacklevel=2 - ) - return self._root.findall(path, namespaces) - - ## - # Finds all matching subelements, by tag name or path. - # Same as getroot().iterfind(path). - # - # @param path What element to look for. - # @keyparam namespaces Optional namespace prefix map. - # @return An iterator or sequence containing all matching elements, - # in document order. - # @defreturn a generated sequence of Element instances - - def iterfind(self, path, namespaces=None): - # assert self._root is not None - if path[:1] == "/": - path = "." + path - warnings.warn( - "This search is broken in 1.3 and earlier, and will be " - "fixed in a future version. If you rely on the current " - "behaviour, change it to %r" % path, - FutureWarning, stacklevel=2 - ) - return self._root.iterfind(path, namespaces) - - ## - # Writes the element tree to a file, as XML. - # - # @def write(file, **options) - # @param file A file name, or a file object opened for writing. - # @param **options Options, given as keyword arguments. - # @keyparam encoding Optional output encoding (default is US-ASCII). - # @keyparam method Optional output method ("xml", "html", "text" or - # "c14n"; default is "xml"). - # @keyparam xml_declaration Controls if an XML declaration should - # be added to the file. Use False for never, True for always, - # None for only if not US-ASCII or UTF-8. None is default. - - def write(self, file_or_filename, - # keyword arguments - encoding=None, - xml_declaration=None, - default_namespace=None, - method=None): - # assert self._root is not None - if not method: - method = "xml" - elif method not in _serialize: - # FIXME: raise an ImportError for c14n if ElementC14N is missing? - raise ValueError("unknown method %r" % method) - if hasattr(file_or_filename, "write"): - file = file_or_filename - else: - file = open(file_or_filename, "wb") - write = file.write - if not encoding: - if method == "c14n": - encoding = "utf-8" - else: - encoding = "us-ascii" - elif xml_declaration or (xml_declaration is None and - encoding not in ("utf-8", "us-ascii")): - if method == "xml": - write("\n" % encoding) - if method == "text": - _serialize_text(write, self._root, encoding) - else: - qnames, namespaces = _namespaces( - self._root, encoding, default_namespace - ) - serialize = _serialize[method] - serialize(write, self._root, encoding, qnames, namespaces) - if file_or_filename is not file: - file.close() - - def write_c14n(self, file): - # lxml.etree compatibility. use output method instead - return self.write(file, method="c14n") - -# -------------------------------------------------------------------- -# serialization support - -def _namespaces(elem, encoding, default_namespace=None): - # identify namespaces used in this tree - - # maps qnames to *encoded* prefix:local names - qnames = {None: None} - - # maps uri:s to prefixes - namespaces = {} - if default_namespace: - namespaces[default_namespace] = "" - - def encode(text): - return text.encode(encoding) - - def add_qname(qname): - # calculate serialized qname representation - try: - if qname[:1] == "{": - uri, tag = qname[1:].rsplit("}", 1) - prefix = namespaces.get(uri) - if prefix is None: - prefix = _namespace_map.get(uri) - if prefix is None: - prefix = "ns%d" % len(namespaces) - if prefix != "xml": - namespaces[uri] = prefix - if prefix: - qnames[qname] = encode("%s:%s" % (prefix, tag)) - else: - qnames[qname] = encode(tag) # default element - else: - if default_namespace: - # FIXME: can this be handled in XML 1.0? - raise ValueError( - "cannot use non-qualified names with " - "default_namespace option" - ) - qnames[qname] = encode(qname) - except TypeError: - _raise_serialization_error(qname) - - # populate qname and namespaces table - try: - iterate = elem.iter - except AttributeError: - iterate = elem.getiterator # cET compatibility - for elem in iterate(): - tag = elem.tag - if isinstance(tag, QName): - if tag.text not in qnames: - add_qname(tag.text) - elif isinstance(tag, basestring): - if tag not in qnames: - add_qname(tag) - elif tag is not None and tag is not Comment and tag is not PI: - _raise_serialization_error(tag) - for key, value in elem.items(): - if isinstance(key, QName): - key = key.text - if key not in qnames: - add_qname(key) - if isinstance(value, QName) and value.text not in qnames: - add_qname(value.text) - text = elem.text - if isinstance(text, QName) and text.text not in qnames: - add_qname(text.text) - return qnames, namespaces - -def _serialize_xml(write, elem, encoding, qnames, namespaces): - tag = elem.tag - text = elem.text - if tag is Comment: - write("" % _encode(text, encoding)) - elif tag is ProcessingInstruction: - write("" % _encode(text, encoding)) - else: - tag = qnames[tag] - if tag is None: - if text: - write(_escape_cdata(text, encoding)) - for e in elem: - _serialize_xml(write, e, encoding, qnames, None) - else: - write("<" + tag) - items = elem.items() - if items or namespaces: - if namespaces: - for v, k in sorted(namespaces.items(), - key=lambda x: x[1]): # sort on prefix - if k: - k = ":" + k - write(" xmlns%s=\"%s\"" % ( - k.encode(encoding), - _escape_attrib(v, encoding) - )) - for k, v in sorted(items): # lexical order - if isinstance(k, QName): - k = k.text - if isinstance(v, QName): - v = qnames[v.text] - else: - v = _escape_attrib(v, encoding) - write(" %s=\"%s\"" % (qnames[k], v)) - if text or len(elem): - write(">") - if text: - write(_escape_cdata(text, encoding)) - for e in elem: - _serialize_xml(write, e, encoding, qnames, None) - write("") - else: - write(" />") - if elem.tail: - write(_escape_cdata(elem.tail, encoding)) - -HTML_EMPTY = ("area", "base", "basefont", "br", "col", "frame", "hr", - "img", "input", "isindex", "link", "meta" "param") - -try: - HTML_EMPTY = set(HTML_EMPTY) -except NameError: - pass - -def _serialize_html(write, elem, encoding, qnames, namespaces): - tag = elem.tag - text = elem.text - if tag is Comment: - write("" % _escape_cdata(text, encoding)) - elif tag is ProcessingInstruction: - write("" % _escape_cdata(text, encoding)) - else: - tag = qnames[tag] - if tag is None: - if text: - write(_escape_cdata(text, encoding)) - for e in elem: - _serialize_html(write, e, encoding, qnames, None) - else: - write("<" + tag) - items = elem.items() - if items or namespaces: - if namespaces: - for v, k in sorted(namespaces.items(), - key=lambda x: x[1]): # sort on prefix - if k: - k = ":" + k - write(" xmlns%s=\"%s\"" % ( - k.encode(encoding), - _escape_attrib(v, encoding) - )) - for k, v in sorted(items): # lexical order - if isinstance(k, QName): - k = k.text - if isinstance(v, QName): - v = qnames[v.text] - else: - v = _escape_attrib_html(v, encoding) - # FIXME: handle boolean attributes - write(" %s=\"%s\"" % (qnames[k], v)) - write(">") - tag = tag.lower() - if text: - if tag == "script" or tag == "style": - write(_encode(text, encoding)) - else: - write(_escape_cdata(text, encoding)) - for e in elem: - _serialize_html(write, e, encoding, qnames, None) - if tag not in HTML_EMPTY: - write("") - if elem.tail: - write(_escape_cdata(elem.tail, encoding)) - -def _serialize_text(write, elem, encoding): - for part in elem.itertext(): - write(part.encode(encoding)) - if elem.tail: - write(elem.tail.encode(encoding)) - -_serialize = { - "xml": _serialize_xml, - "html": _serialize_html, - "text": _serialize_text, -# this optional method is imported at the end of the module -# "c14n": _serialize_c14n, -} - -## -# Registers a namespace prefix. The registry is global, and any -# existing mapping for either the given prefix or the namespace URI -# will be removed. -# -# @param prefix Namespace prefix. -# @param uri Namespace uri. Tags and attributes in this namespace -# will be serialized with the given prefix, if at all possible. -# @exception ValueError If the prefix is reserved, or is otherwise -# invalid. - -def register_namespace(prefix, uri): - if re.match("ns\d+$", prefix): - raise ValueError("Prefix format reserved for internal use") - for k, v in _namespace_map.items(): - if k == uri or v == prefix: - del _namespace_map[k] - _namespace_map[uri] = prefix - -_namespace_map = { - # "well-known" namespace prefixes - "http://www.w3.org/XML/1998/namespace": "xml", - "http://www.w3.org/1999/xhtml": "html", - "http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf", - "http://schemas.xmlsoap.org/wsdl/": "wsdl", - # xml schema - "http://www.w3.org/2001/XMLSchema": "xs", - "http://www.w3.org/2001/XMLSchema-instance": "xsi", - # dublin core - "http://purl.org/dc/elements/1.1/": "dc", -} - -def _raise_serialization_error(text): - raise TypeError( - "cannot serialize %r (type %s)" % (text, type(text).__name__) - ) - -def _encode(text, encoding): - try: - return text.encode(encoding, "xmlcharrefreplace") - except (TypeError, AttributeError): - _raise_serialization_error(text) - -def _escape_cdata(text, encoding): - # escape character data - try: - # it's worth avoiding do-nothing calls for strings that are - # shorter than 500 character, or so. assume that's, by far, - # the most common case in most applications. - if "&" in text: - text = text.replace("&", "&") - if "<" in text: - text = text.replace("<", "<") - if ">" in text: - text = text.replace(">", ">") - return text.encode(encoding, "xmlcharrefreplace") - except (TypeError, AttributeError): - _raise_serialization_error(text) - -def _escape_attrib(text, encoding): - # escape attribute value - try: - if "&" in text: - text = text.replace("&", "&") - if "<" in text: - text = text.replace("<", "<") - if ">" in text: - text = text.replace(">", ">") - if "\"" in text: - text = text.replace("\"", """) - if "\n" in text: - text = text.replace("\n", " ") - return text.encode(encoding, "xmlcharrefreplace") - except (TypeError, AttributeError): - _raise_serialization_error(text) - -def _escape_attrib_html(text, encoding): - # escape attribute value - try: - if "&" in text: - text = text.replace("&", "&") - if ">" in text: - text = text.replace(">", ">") - if "\"" in text: - text = text.replace("\"", """) - return text.encode(encoding, "xmlcharrefreplace") - except (TypeError, AttributeError): - _raise_serialization_error(text) - -# -------------------------------------------------------------------- - -## -# Generates a string representation of an XML element, including all -# subelements. -# -# @param element An Element instance. -# @keyparam encoding Optional output encoding (default is US-ASCII). -# @keyparam method Optional output method ("xml", "html", "text" or -# "c14n"; default is "xml"). -# @return An encoded string containing the XML data. -# @defreturn string - -def tostring(element, encoding=None, method=None): - class dummy: - pass - data = [] - file = dummy() - file.write = data.append - ElementTree(element).write(file, encoding, method=method) - return "".join(data) - -## -# Generates a string representation of an XML element, including all -# subelements. The string is returned as a sequence of string fragments. -# -# @param element An Element instance. -# @keyparam encoding Optional output encoding (default is US-ASCII). -# @keyparam method Optional output method ("xml", "html", "text" or -# "c14n"; default is "xml"). -# @return A sequence object containing the XML data. -# @defreturn sequence -# @since 1.3 - -def tostringlist(element, encoding=None, method=None): - class dummy: - pass - data = [] - file = dummy() - file.write = data.append - ElementTree(element).write(file, encoding, method=method) - # FIXME: merge small fragments into larger parts - return data - -## -# Writes an element tree or element structure to sys.stdout. This -# function should be used for debugging only. -#

-# The exact output format is implementation dependent. In this -# version, it's written as an ordinary XML file. -# -# @param elem An element tree or an individual element. - -def dump(elem): - # debugging - if not isinstance(elem, ElementTree): - elem = ElementTree(elem) - elem.write(sys.stdout) - tail = elem.getroot().tail - if not tail or tail[-1] != "\n": - sys.stdout.write("\n") - -# -------------------------------------------------------------------- -# parsing - -## -# Parses an XML document into an element tree. -# -# @param source A filename or file object containing XML data. -# @param parser An optional parser instance. If not given, the -# standard {@link XMLParser} parser is used. -# @return An ElementTree instance - -def parse(source, parser=None): - tree = ElementTree() - tree.parse(source, parser) - return tree - -## -# Parses an XML document into an element tree incrementally, and reports -# what's going on to the user. -# -# @param source A filename or file object containing XML data. -# @param events A list of events to report back. If omitted, only "end" -# events are reported. -# @param parser An optional parser instance. If not given, the -# standard {@link XMLParser} parser is used. -# @return A (event, elem) iterator. - -def iterparse(source, events=None, parser=None): - if not hasattr(source, "read"): - source = open(source, "rb") - if not parser: - parser = XMLParser(target=TreeBuilder()) - return _IterParseIterator(source, events, parser) - -class _IterParseIterator(object): - - def __init__(self, source, events, parser): - self._file = source - self._events = [] - self._index = 0 - self.root = self._root = None - self._parser = parser - # wire up the parser for event reporting - parser = self._parser._parser - append = self._events.append - if events is None: - events = ["end"] - for event in events: - if event == "start": - try: - parser.ordered_attributes = 1 - parser.specified_attributes = 1 - def handler(tag, attrib_in, event=event, append=append, - start=self._parser._start_list): - append((event, start(tag, attrib_in))) - parser.StartElementHandler = handler - except AttributeError: - def handler(tag, attrib_in, event=event, append=append, - start=self._parser._start): - append((event, start(tag, attrib_in))) - parser.StartElementHandler = handler - elif event == "end": - def handler(tag, event=event, append=append, - end=self._parser._end): - append((event, end(tag))) - parser.EndElementHandler = handler - elif event == "start-ns": - def handler(prefix, uri, event=event, append=append): - try: - uri = (uri or "").encode("ascii") - except UnicodeError: - pass - append((event, (prefix or "", uri or ""))) - parser.StartNamespaceDeclHandler = handler - elif event == "end-ns": - def handler(prefix, event=event, append=append): - append((event, None)) - parser.EndNamespaceDeclHandler = handler - else: - raise ValueError("unknown event %r" % event) - - def next(self): - while 1: - try: - item = self._events[self._index] - except IndexError: - if self._parser is None: - self.root = self._root - raise StopIteration - # load event buffer - del self._events[:] - self._index = 0 - data = self._file.read(16384) - if data: - self._parser.feed(data) - else: - self._root = self._parser.close() - self._parser = None - else: - self._index = self._index + 1 - return item - - def __iter__(self): - return self - -## -# Parses an XML document from a string constant. This function can -# be used to embed "XML literals" in Python code. -# -# @param source A string containing XML data. -# @param parser An optional parser instance. If not given, the -# standard {@link XMLParser} parser is used. -# @return An Element instance. -# @defreturn Element - -def XML(text, parser=None): - if not parser: - parser = XMLParser(target=TreeBuilder()) - parser.feed(text) - return parser.close() - -## -# Parses an XML document from a string constant, and also returns -# a dictionary which maps from element id:s to elements. -# -# @param source A string containing XML data. -# @param parser An optional parser instance. If not given, the -# standard {@link XMLParser} parser is used. -# @return A tuple containing an Element instance and a dictionary. -# @defreturn (Element, dictionary) - -def XMLID(text, parser=None): - if not parser: - parser = XMLParser(target=TreeBuilder()) - parser.feed(text) - tree = parser.close() - ids = {} - for elem in tree.iter(): - id = elem.get("id") - if id: - ids[id] = elem - return tree, ids - -## -# Parses an XML document from a string constant. Same as {@link #XML}. -# -# @def fromstring(text) -# @param source A string containing XML data. -# @return An Element instance. -# @defreturn Element - -fromstring = XML - -## -# Parses an XML document from a sequence of string fragments. -# -# @param sequence A list or other sequence containing XML data fragments. -# @param parser An optional parser instance. If not given, the -# standard {@link XMLParser} parser is used. -# @return An Element instance. -# @defreturn Element -# @since 1.3 - -def fromstringlist(sequence, parser=None): - if not parser: - parser = XMLParser(target=TreeBuilder()) - for text in sequence: - parser.feed(text) - return parser.close() - -# -------------------------------------------------------------------- - -## -# Generic element structure builder. This builder converts a sequence -# of {@link #TreeBuilder.start}, {@link #TreeBuilder.data}, and {@link -# #TreeBuilder.end} method calls to a well-formed element structure. -#

-# You can use this class to build an element structure using a custom XML -# parser, or a parser for some other XML-like format. -# -# @param element_factory Optional element factory. This factory -# is called to create new Element instances, as necessary. - -class TreeBuilder(object): - - def __init__(self, element_factory=None): - self._data = [] # data collector - self._elem = [] # element stack - self._last = None # last element - self._tail = None # true if we're after an end tag - if element_factory is None: - element_factory = Element - self._factory = element_factory - - ## - # Flushes the builder buffers, and returns the toplevel document - # element. - # - # @return An Element instance. - # @defreturn Element - - def close(self): - assert len(self._elem) == 0, "missing end tags" - assert self._last is not None, "missing toplevel element" - return self._last - - def _flush(self): - if self._data: - if self._last is not None: - text = "".join(self._data) - if self._tail: - assert self._last.tail is None, "internal error (tail)" - self._last.tail = text - else: - assert self._last.text is None, "internal error (text)" - self._last.text = text - self._data = [] - - ## - # Adds text to the current element. - # - # @param data A string. This should be either an 8-bit string - # containing ASCII text, or a Unicode string. - - def data(self, data): - self._data.append(data) - - ## - # Opens a new element. - # - # @param tag The element name. - # @param attrib A dictionary containing element attributes. - # @return The opened element. - # @defreturn Element - - def start(self, tag, attrs): - self._flush() - self._last = elem = self._factory(tag, attrs) - if self._elem: - self._elem[-1].append(elem) - self._elem.append(elem) - self._tail = 0 - return elem - - ## - # Closes the current element. - # - # @param tag The element name. - # @return The closed element. - # @defreturn Element - - def end(self, tag): - self._flush() - self._last = self._elem.pop() - assert self._last.tag == tag,\ - "end tag mismatch (expected %s, got %s)" % ( - self._last.tag, tag) - self._tail = 1 - return self._last - -## -# Element structure builder for XML source data, based on the -# expat parser. -# -# @keyparam target Target object. If omitted, the builder uses an -# instance of the standard {@link #TreeBuilder} class. -# @keyparam html Predefine HTML entities. This flag is not supported -# by the current implementation. -# @keyparam encoding Optional encoding. If given, the value overrides -# the encoding specified in the XML file. -# @see #ElementTree -# @see #TreeBuilder - -class XMLParser(object): - - def __init__(self, html=0, target=None, encoding=None): - try: - from xml.parsers import expat - except ImportError: - try: - import pyexpat as expat - except ImportError: - raise ImportError( - "No module named expat; use SimpleXMLTreeBuilder instead" - ) - parser = expat.ParserCreate(encoding, "}") - if target is None: - target = TreeBuilder() - # underscored names are provided for compatibility only - self.parser = self._parser = parser - self.target = self._target = target - self._error = expat.error - self._names = {} # name memo cache - # callbacks - parser.DefaultHandlerExpand = self._default - parser.StartElementHandler = self._start - parser.EndElementHandler = self._end - parser.CharacterDataHandler = self._data - # optional callbacks - parser.CommentHandler = self._comment - parser.ProcessingInstructionHandler = self._pi - # let expat do the buffering, if supported - try: - self._parser.buffer_text = 1 - except AttributeError: - pass - # use new-style attribute handling, if supported - try: - self._parser.ordered_attributes = 1 - self._parser.specified_attributes = 1 - parser.StartElementHandler = self._start_list - except AttributeError: - pass - self._doctype = None - self.entity = {} - try: - self.version = "Expat %d.%d.%d" % expat.version_info - except AttributeError: - pass # unknown - - def _raiseerror(self, value): - err = ParseError(value) - err.code = value.code - err.position = value.lineno, value.offset - raise err - - def _fixtext(self, text): - # convert text string to ascii, if possible - try: - return text.encode("ascii") - except UnicodeError: - return text - - def _fixname(self, key): - # expand qname, and convert name string to ascii, if possible - try: - name = self._names[key] - except KeyError: - name = key - if "}" in name: - name = "{" + name - self._names[key] = name = self._fixtext(name) - return name - - def _start(self, tag, attrib_in): - fixname = self._fixname - fixtext = self._fixtext - tag = fixname(tag) - attrib = {} - for key, value in attrib_in.items(): - attrib[fixname(key)] = fixtext(value) - return self.target.start(tag, attrib) - - def _start_list(self, tag, attrib_in): - fixname = self._fixname - fixtext = self._fixtext - tag = fixname(tag) - attrib = {} - if attrib_in: - for i in range(0, len(attrib_in), 2): - attrib[fixname(attrib_in[i])] = fixtext(attrib_in[i+1]) - return self.target.start(tag, attrib) - - def _data(self, text): - return self.target.data(self._fixtext(text)) - - def _end(self, tag): - return self.target.end(self._fixname(tag)) - - def _comment(self, data): - try: - comment = self.target.comment - except AttributeError: - pass - else: - return comment(self._fixtext(data)) - - def _pi(self, target, data): - try: - pi = self.target.pi - except AttributeError: - pass - else: - return pi(self._fixtext(target), self._fixtext(data)) - - def _default(self, text): - prefix = text[:1] - if prefix == "&": - # deal with undefined entities - try: - self.target.data(self.entity[text[1:-1]]) - except KeyError: - from xml.parsers import expat - err = expat.error( - "undefined entity %s: line %d, column %d" % - (text, self._parser.ErrorLineNumber, - self._parser.ErrorColumnNumber) - ) - err.code = 11 # XML_ERROR_UNDEFINED_ENTITY - err.lineno = self._parser.ErrorLineNumber - err.offset = self._parser.ErrorColumnNumber - raise err - elif prefix == "<" and text[:9] == "": - self._doctype = None - return - text = text.strip() - if not text: - return - self._doctype.append(text) - n = len(self._doctype) - if n > 2: - type = self._doctype[1] - if type == "PUBLIC" and n == 4: - name, type, pubid, system = self._doctype - elif type == "SYSTEM" and n == 3: - name, type, system = self._doctype - pubid = None - else: - return - if pubid: - pubid = pubid[1:-1] - if hasattr(self.target, "doctype"): - self.target.doctype(name, pubid, system[1:-1]) - elif self.doctype is not self._XMLParser__doctype: - # warn about deprecated call - self._XMLParser__doctype(name, pubid, system[1:-1]) - self.doctype(name, pubid, system[1:-1]) - self._doctype = None - - ## - # (Deprecated) Handles a doctype declaration. - # - # @param name Doctype name. - # @param pubid Public identifier. - # @param system System identifier. - - def doctype(self, name, pubid, system): - """This method of XMLParser is deprecated.""" - warnings.warn( - "This method of XMLParser is deprecated. Define doctype() " - "method on the TreeBuilder target.", - DeprecationWarning, - ) - - # sentinel, if doctype is redefined in a subclass - __doctype = doctype - - ## - # Feeds data to the parser. - # - # @param data Encoded data. - - def feed(self, data): - try: - self._parser.Parse(data, 0) - except self._error, v: - self._raiseerror(v) - - ## - # Finishes feeding data to the parser. - # - # @return An element structure. - # @defreturn Element - - def close(self): - try: - self._parser.Parse("", 1) # end of data - except self._error, v: - self._raiseerror(v) - tree = self.target.close() - del self.target, self._parser # get rid of circular references - return tree - -# compatibility -XMLTreeBuilder = XMLParser - -# workaround circular import. -try: - from ElementC14N import _serialize_c14n - _serialize["c14n"] = _serialize_c14n -except ImportError: - pass diff --git a/addon/xml2/etree/cElementTree.py b/addon/xml2/etree/cElementTree.py deleted file mode 100644 index a6f127ab..00000000 --- a/addon/xml2/etree/cElementTree.py +++ /dev/null @@ -1,3 +0,0 @@ -# Wrapper module for _elementtree - -from _elementtree import * diff --git a/addon/xml2/parsers/__init__.pyo b/addon/xml2/parsers/__init__.pyo deleted file mode 100644 index 0fa64587..00000000 Binary files a/addon/xml2/parsers/__init__.pyo and /dev/null differ diff --git a/addon/xml2/parsers/expat.py b/addon/xml2/parsers/expat.py deleted file mode 100644 index 11359a0b..00000000 --- a/addon/xml2/parsers/expat.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Interface to the Expat non-validating XML parser.""" -__version__ = '$Revision$' - -from pyexpat import * diff --git a/addon/xml2/parsers/expat.pyo b/addon/xml2/parsers/expat.pyo deleted file mode 100644 index 30233791..00000000 Binary files a/addon/xml2/parsers/expat.pyo and /dev/null differ diff --git a/buildVars.py b/buildVars.py index 51eaf278..0ccc5a84 100644 --- a/buildVars.py +++ b/buildVars.py @@ -19,7 +19,7 @@ # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Add-on for using NVDA as a feed reader."""), # version - "addon_version" : "8.0", + "addon_version" : "9.0", # Author(s) "addon_author" : u"Noelia Ruiz Martínez , Mesar Hameed ", # URL for the add-on documentation support @@ -27,9 +27,9 @@ # Documentation file name "addon_docFileName" : "readme.html", # Minimum NVDA version supported (e.g. "2018.3") - "addon_minimumNVDAVersion" : "2018.3.0", + "addon_minimumNVDAVersion" : "2019.3", # Last NVDA version supported/tested (e.g. "2018.4", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion" : "2019.2.0", + "addon_lastTestedNVDAVersion" : "2019.3", # Add-on update channel (default is stable or None) "addon_updateChannel" : None, } diff --git a/readme.md b/readme.md index 893a6cbd..2a460a56 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # Read Feeds # * Authors: Noelia Ruiz Martínez, Mesar Hameed -* NVDA compatibility: 2018.3 to 2019.2 +* NVDA compatibility: 2019.3 or later * Download [stable version][1] * Download [development version][2] @@ -64,6 +64,10 @@ Opens a dialog to select a folder which replaces your feeds in the personalFeeds * NVDA will display an error message if it was not possible to backup or restore the personalFeeds folder. * The title of the articles list dialog displays the selected feed name and number of items available. +## Changes for 9.0 ## + +* Requires NVDA 2019.3 or later. + ## Changes for 8.0 ## * When the add-on is updated, feeds saved in the previous version of the add-on will be automatically copied to the new version, unless you prefer to import feeds saved in the main configuration folder of NVDA. diff --git a/sconstruct b/sconstruct index d1e3c76d..e890d971 100644 --- a/sconstruct +++ b/sconstruct @@ -3,7 +3,6 @@ #This file is covered by the GNU General Public License. #See the file COPYING.txt for more details. -import codecs import gettext import os import os.path @@ -19,7 +18,7 @@ def md2html(source, dest): lang = os.path.basename(os.path.dirname(source)).replace('_', '-') localeLang = os.path.basename(os.path.dirname(source)) try: - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).ugettext if sys.version_info.major == 2 else gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext title=u"{0}".format(_(buildVars.addon_info["addon_summary"])) except: title="{0}".format(buildVars.addon_info["addon_summary"]) @@ -27,20 +26,18 @@ def md2html(source, dest): "[[!meta title=\"": "# ", "\"]]": " #", } - with codecs.open(source, "r", "utf-8") as f: + with open(source, 'r', encoding="utf-8") as f: mdText = f.read() - headerList = headerDic.iteritems () if sys.version_info.major == 2 else list(headerDic.items()) - for k, v in headerList: + for k, v in headerDic.items(): mdText = mdText.replace(k, v, 1) htmlText = markdown.markdown(mdText) - with codecs.open(dest, "w", "utf-8") as f: - f.write("\n" + - "\n" + - "\n" % (lang, lang) + + with open(dest, 'w', encoding="utf-8") as f: + f.write("\n" + + "\n" % lang + "\n" + - "\n" + - "\n" + + "\n" + + "\n" + + "\n" + "%s\n" % title + "\n\n" ) @@ -109,21 +106,27 @@ def createAddonBundleFromPath(path, dest): return dest def generateManifest(source, dest): - with codecs.open(source, "r", "utf-8") as f: + with open(source, 'r', encoding="utf-8") as f: manifest_template = f.read() manifest = manifest_template.format(**buildVars.addon_info) - with codecs.open(dest, "w", "utf-8") as f: + with open(dest, 'w', encoding="utf-8") as f: f.write(manifest) def generateTranslatedManifest(source, language, out): - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).ugettext if sys.version_info.major == 2 else gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext vars = {} for var in ("addon_summary", "addon_description"): - vars[var] = _(buildVars.addon_info[var]) - with codecs.open(source, "r", "utf-8") as f: + if isinstance(buildVars.addon_info[var], str): + vars[var] = _(buildVars.addon_info[var]) + elif isinstance(buildVars.addon_info[var], list): + vars[var] = ''.join([_(l) for l in buildVars.addon_info[var]]) + else: + raise TypeError("Error with %s key in buildVars" % var) + + with open(source, 'r', encoding="utf-8") as f: manifest_template = f.read() result = manifest_template.format(**vars) - with codecs.open(out, "w", "utf-8") as f: + with open(out, 'w', encoding="utf-8") as f: f.write(result) def expandGlobs(files): @@ -167,10 +170,10 @@ for dir in langDirs: translatedManifest = env.NVDATranslatedManifest(dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")]) env.Depends(translatedManifest, ["buildVars.py"]) env.Depends(addon, [translatedManifest, moFile]) - #Convert markdown files to html - for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')): - htmlFile = env.markdown(mdFile) - env.Depends(htmlFile, [mdFile, moFile]) - env.Depends(addon, htmlFile) +#Convert markdown files to html +for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')): + htmlFile = env.markdown(mdFile) + env.Depends(htmlFile, mdFile) + env.Depends(addon, htmlFile) env.Default(addon) env.Clean (addon, ['.sconsign.dblite', 'addon/doc/en/'])