123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085 |
- """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 https://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",
- "indent", "iselement", "iterparse",
- "parse", "ParseError",
- "PI", "ProcessingInstruction",
- "QName",
- "SubElement",
- "tostring", "tostringlist",
- "TreeBuilder",
- "VERSION",
- "XML", "XMLID",
- "XMLParser", "XMLPullParser",
- "register_namespace",
- "canonicalize", "C14NWriterTarget",
- ]
- VERSION = "1.3.0"
- import sys
- import re
- import warnings
- import io
- import collections
- import collections.abc
- import contextlib
- import weakref
- 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:
- <tag attrib>text<child/>...</tag>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__,))
- self.tag = tag
- self.attrib = {**attrib, **extra}
- 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):
- 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(
- "Testing an element's truth value will always return True in "
- "future versions. "
- "Use specific 'len(elem)' or 'elem is not None' test instead.",
- DeprecationWarning, 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:
- self._assert_is_element(elt)
- else:
- self._assert_is_element(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.append(element)
- 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 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)
- 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, **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 data := source.read(65536):
- 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)
- 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"
- with _get_writer(file_or_filename, encoding) as (write, declared_encoding):
- if method == "xml" and (xml_declaration or
- (xml_declaration is None and
- encoding.lower() != "unicode" and
- declared_encoding.lower() not in ("utf-8", "us-ascii"))):
- write("<?xml version='1.0' encoding='%s'?>\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.lower() == "unicode":
- encoding="utf-8"
- with open(file_or_filename, "w", encoding=encoding,
- errors="xmlcharrefreplace") as file:
- yield file.write, encoding
- else:
- # file_or_filename is a file-like object
- # encoding determines if it is a text or binary writer
- if encoding.lower() == "unicode":
- # use a text writer as is
- yield write, getattr(file_or_filename, "encoding", None) or "utf-8"
- 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, encoding
- 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("<!--%s-->" % text)
- elif tag is ProcessingInstruction:
- write("<?%s?>" % 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 items:
- 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("</" + tag + ">")
- else:
- write(" />")
- if elem.tail:
- write(_escape_cdata(elem.tail))
- HTML_EMPTY = {"area", "base", "basefont", "br", "col", "embed", "frame", "hr",
- "img", "input", "isindex", "link", "meta", "param", "source",
- "track", "wbr"}
- def _serialize_html(write, elem, qnames, namespaces, **kwargs):
- tag = elem.tag
- text = elem.text
- if tag is Comment:
- write("<!--%s-->" % _escape_cdata(text))
- elif tag is ProcessingInstruction:
- write("<?%s?>" % _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 items:
- 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("</" + tag + ">")
- 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("\"", """)
- # Although section 2.11 of the XML specification states that CR or
- # CR LN should be replaced with just LN, it applies only to EOLNs
- # which take part of organizing file into lines. Within attributes,
- # we are replacing these with entity numbers, so they do not count.
- # http://www.w3.org/TR/REC-xml/#sec-line-ends
- # The current solution, contained in following six lines, was
- # discussed in issue 17582 and 39011.
- if "\r" in text:
- text = text.replace("\r", " ")
- 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, *,
- xml_declaration=None, default_namespace=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", *default_namespace*
- sets the default XML namespace (for "xmlns").
- Returns an (optionally) encoded string containing the XML data.
- """
- stream = io.StringIO() if encoding == 'unicode' else io.BytesIO()
- ElementTree(element).write(stream, encoding,
- xml_declaration=xml_declaration,
- default_namespace=default_namespace,
- 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, *,
- xml_declaration=None, default_namespace=None,
- short_empty_elements=True):
- lst = []
- stream = _ListDataStream(lst)
- ElementTree(element).write(stream, encoding,
- xml_declaration=xml_declaration,
- default_namespace=default_namespace,
- 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")
- def indent(tree, space=" ", level=0):
- """Indent an XML document by inserting newlines and indentation space
- after elements.
- *tree* is the ElementTree or Element to modify. The (root) element
- itself will not be changed, but the tail text of all elements in its
- subtree will be adapted.
- *space* is the whitespace to insert for each indentation level, two
- space characters by default.
- *level* is the initial indentation level. Setting this to a higher
- value than 0 can be used for indenting subtrees that are more deeply
- nested inside of a document.
- """
- if isinstance(tree, ElementTree):
- tree = tree.getroot()
- if level < 0:
- raise ValueError(f"Initial indentation level must be >= 0, got {level}")
- if not len(tree):
- return
- # Reduce the memory consumption by reusing indentation strings.
- indentations = ["\n" + level * space]
- def _indent_children(elem, level):
- # Start a new indentation level for the first child.
- child_level = level + 1
- try:
- child_indentation = indentations[child_level]
- except IndexError:
- child_indentation = indentations[level] + space
- indentations.append(child_indentation)
- if not elem.text or not elem.text.strip():
- elem.text = child_indentation
- for child in elem:
- if len(child):
- _indent_children(child, child_level)
- if not child.tail or not child.tail.strip():
- child.tail = child_indentation
- # Dedent after the last child by overwriting the previous indentation.
- if not child.tail.strip():
- child.tail = indentations[level]
- _indent_children(tree, 0)
- # --------------------------------------------------------------------
- # 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)
- if not hasattr(source, "read"):
- source = open(source, "rb")
- close_source = True
- else:
- close_source = False
- def iterator(source):
- 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 = wr()
- if it is not None:
- it.root = root
- finally:
- if close_source:
- source.close()
- class IterParseIterator(collections.abc.Iterator):
- __next__ = iterator(source).__next__
- def __del__(self):
- if close_source:
- source.close()
- it = IterParseIterator()
- it.root = None
- wr = weakref.ref(it)
- 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 https://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 flush(self):
- if self._parser is None:
- raise ValueError("flush() called after end of stream")
- self._parser.flush()
- 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.
- *comment_factory* is a factory to create comments to be used instead of
- the standard factory. If *insert_comments* is false (the default),
- comments will not be inserted into the tree.
- *pi_factory* is a factory to create processing instructions to be used
- instead of the standard factory. If *insert_pis* is false (the default),
- processing instructions will not be inserted into the tree.
- """
- def __init__(self, element_factory=None, *,
- comment_factory=None, pi_factory=None,
- insert_comments=False, insert_pis=False):
- self._data = [] # data collector
- self._elem = [] # element stack
- self._last = None # last element
- self._root = None # root element
- self._tail = None # true if we're after an end tag
- if comment_factory is None:
- comment_factory = Comment
- self._comment_factory = comment_factory
- self.insert_comments = insert_comments
- if pi_factory is None:
- pi_factory = ProcessingInstruction
- self._pi_factory = pi_factory
- self.insert_pis = insert_pis
- 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._root is not None, "missing toplevel element"
- return self._root
- 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)
- elif self._root is None:
- self._root = 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
- def comment(self, text):
- """Create a comment using the comment_factory.
- *text* is the text of the comment.
- """
- return self._handle_single(
- self._comment_factory, self.insert_comments, text)
- def pi(self, target, text=None):
- """Create a processing instruction using the pi_factory.
- *target* is the target name of the processing instruction.
- *text* is the data of the processing instruction, or ''.
- """
- return self._handle_single(
- self._pi_factory, self.insert_pis, target, text)
- def _handle_single(self, factory, insert, *args):
- elem = factory(*args)
- if insert:
- self._flush()
- self._last = elem
- if self._elem:
- self._elem[-1].append(elem)
- self._tail = 1
- return elem
- # also see ElementTree and TreeBuilder
- class XMLParser:
- """Element structure builder for XML source data based on the expat parser.
- *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, *, 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
- # 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, 'start_ns'):
- parser.StartNamespaceDeclHandler = self._start_ns
- if hasattr(target, 'end_ns'):
- parser.EndNamespaceDeclHandler = self._end_ns
- 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
- 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
- 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":
- # TreeBuilder does not implement .start_ns()
- if hasattr(self.target, "start_ns"):
- def handler(prefix, uri, event=event_name, append=append,
- start_ns=self._start_ns):
- append((event, start_ns(prefix, uri)))
- else:
- def handler(prefix, uri, event=event_name, append=append):
- append((event, (prefix or '', uri or '')))
- parser.StartNamespaceDeclHandler = handler
- elif event_name == "end-ns":
- # TreeBuilder does not implement .end_ns()
- if hasattr(self.target, "end_ns"):
- def handler(prefix, event=event_name, append=append,
- end_ns=self._end_ns):
- append((event, end_ns(prefix)))
- else:
- def handler(prefix, event=event_name, append=append):
- append((event, None))
- parser.EndNamespaceDeclHandler = handler
- elif event_name == 'comment':
- def handler(text, event=event_name, append=append, self=self):
- append((event, self.target.comment(text)))
- parser.CommentHandler = handler
- elif event_name == 'pi':
- def handler(pi_target, data, event=event_name, append=append,
- self=self):
- append((event, self.target.pi(pi_target, data)))
- parser.ProcessingInstructionHandler = 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_ns(self, prefix, uri):
- return self.target.start_ns(prefix or '', uri or '')
- def _end_ns(self, prefix):
- return self.target.end_ns(prefix or '')
- 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] == "<!DOCTYPE":
- self._doctype = [] # inside a doctype declaration
- elif self._doctype is not None:
- # parse doctype contents
- if prefix == ">":
- 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 hasattr(self, "doctype"):
- warnings.warn(
- "The doctype() method of XMLParser is ignored. "
- "Define doctype() method on the TreeBuilder target.",
- RuntimeWarning)
- self._doctype = None
- def feed(self, data):
- """Feed encoded data to parser."""
- try:
- self.parser.Parse(data, False)
- except self._error as v:
- self._raiseerror(v)
- def close(self):
- """Finish feeding data to parser and return element structure."""
- try:
- self.parser.Parse(b"", True) # 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
- def flush(self):
- was_enabled = self.parser.GetReparseDeferralEnabled()
- try:
- self.parser.SetReparseDeferralEnabled(False)
- self.parser.Parse(b"", False)
- except self._error as v:
- self._raiseerror(v)
- finally:
- self.parser.SetReparseDeferralEnabled(was_enabled)
- # --------------------------------------------------------------------
- # C14N 2.0
- def canonicalize(xml_data=None, *, out=None, from_file=None, **options):
- """Convert XML to its C14N 2.0 serialised form.
- If *out* is provided, it must be a file or file-like object that receives
- the serialised canonical XML output (text, not bytes) through its ``.write()``
- method. To write to a file, open it in text mode with encoding "utf-8".
- If *out* is not provided, this function returns the output as text string.
- Either *xml_data* (an XML string) or *from_file* (a file path or
- file-like object) must be provided as input.
- The configuration options are the same as for the ``C14NWriterTarget``.
- """
- if xml_data is None and from_file is None:
- raise ValueError("Either 'xml_data' or 'from_file' must be provided as input")
- sio = None
- if out is None:
- sio = out = io.StringIO()
- parser = XMLParser(target=C14NWriterTarget(out.write, **options))
- if xml_data is not None:
- parser.feed(xml_data)
- parser.close()
- elif from_file is not None:
- parse(from_file, parser=parser)
- return sio.getvalue() if sio is not None else None
- _looks_like_prefix_name = re.compile(r'^\w+:\w+$', re.UNICODE).match
- class C14NWriterTarget:
- """
- Canonicalization writer target for the XMLParser.
- Serialises parse events to XML C14N 2.0.
- The *write* function is used for writing out the resulting data stream
- as text (not bytes). To write to a file, open it in text mode with encoding
- "utf-8" and pass its ``.write`` method.
- Configuration options:
- - *with_comments*: set to true to include comments
- - *strip_text*: set to true to strip whitespace before and after text content
- - *rewrite_prefixes*: set to true to replace namespace prefixes by "n{number}"
- - *qname_aware_tags*: a set of qname aware tag names in which prefixes
- should be replaced in text content
- - *qname_aware_attrs*: a set of qname aware attribute names in which prefixes
- should be replaced in text content
- - *exclude_attrs*: a set of attribute names that should not be serialised
- - *exclude_tags*: a set of tag names that should not be serialised
- """
- def __init__(self, write, *,
- with_comments=False, strip_text=False, rewrite_prefixes=False,
- qname_aware_tags=None, qname_aware_attrs=None,
- exclude_attrs=None, exclude_tags=None):
- self._write = write
- self._data = []
- self._with_comments = with_comments
- self._strip_text = strip_text
- self._exclude_attrs = set(exclude_attrs) if exclude_attrs else None
- self._exclude_tags = set(exclude_tags) if exclude_tags else None
- self._rewrite_prefixes = rewrite_prefixes
- if qname_aware_tags:
- self._qname_aware_tags = set(qname_aware_tags)
- else:
- self._qname_aware_tags = None
- if qname_aware_attrs:
- self._find_qname_aware_attrs = set(qname_aware_attrs).intersection
- else:
- self._find_qname_aware_attrs = None
- # Stack with globally and newly declared namespaces as (uri, prefix) pairs.
- self._declared_ns_stack = [[
- ("http://www.w3.org/XML/1998/namespace", "xml"),
- ]]
- # Stack with user declared namespace prefixes as (uri, prefix) pairs.
- self._ns_stack = []
- if not rewrite_prefixes:
- self._ns_stack.append(list(_namespace_map.items()))
- self._ns_stack.append([])
- self._prefix_map = {}
- self._preserve_space = [False]
- self._pending_start = None
- self._root_seen = False
- self._root_done = False
- self._ignored_depth = 0
- def _iter_namespaces(self, ns_stack, _reversed=reversed):
- for namespaces in _reversed(ns_stack):
- if namespaces: # almost no element declares new namespaces
- yield from namespaces
- def _resolve_prefix_name(self, prefixed_name):
- prefix, name = prefixed_name.split(':', 1)
- for uri, p in self._iter_namespaces(self._ns_stack):
- if p == prefix:
- return f'{{{uri}}}{name}'
- raise ValueError(f'Prefix {prefix} of QName "{prefixed_name}" is not declared in scope')
- def _qname(self, qname, uri=None):
- if uri is None:
- uri, tag = qname[1:].rsplit('}', 1) if qname[:1] == '{' else ('', qname)
- else:
- tag = qname
- prefixes_seen = set()
- for u, prefix in self._iter_namespaces(self._declared_ns_stack):
- if u == uri and prefix not in prefixes_seen:
- return f'{prefix}:{tag}' if prefix else tag, tag, uri
- prefixes_seen.add(prefix)
- # Not declared yet => add new declaration.
- if self._rewrite_prefixes:
- if uri in self._prefix_map:
- prefix = self._prefix_map[uri]
- else:
- prefix = self._prefix_map[uri] = f'n{len(self._prefix_map)}'
- self._declared_ns_stack[-1].append((uri, prefix))
- return f'{prefix}:{tag}', tag, uri
- if not uri and '' not in prefixes_seen:
- # No default namespace declared => no prefix needed.
- return tag, tag, uri
- for u, prefix in self._iter_namespaces(self._ns_stack):
- if u == uri:
- self._declared_ns_stack[-1].append((uri, prefix))
- return f'{prefix}:{tag}' if prefix else tag, tag, uri
- if not uri:
- # As soon as a default namespace is defined,
- # anything that has no namespace (and thus, no prefix) goes there.
- return tag, tag, uri
- raise ValueError(f'Namespace "{uri}" is not declared in scope')
- def data(self, data):
- if not self._ignored_depth:
- self._data.append(data)
- def _flush(self, _join_text=''.join):
- data = _join_text(self._data)
- del self._data[:]
- if self._strip_text and not self._preserve_space[-1]:
- data = data.strip()
- if self._pending_start is not None:
- args, self._pending_start = self._pending_start, None
- qname_text = data if data and _looks_like_prefix_name(data) else None
- self._start(*args, qname_text)
- if qname_text is not None:
- return
- if data and self._root_seen:
- self._write(_escape_cdata_c14n(data))
- def start_ns(self, prefix, uri):
- if self._ignored_depth:
- return
- # we may have to resolve qnames in text content
- if self._data:
- self._flush()
- self._ns_stack[-1].append((uri, prefix))
- def start(self, tag, attrs):
- if self._exclude_tags is not None and (
- self._ignored_depth or tag in self._exclude_tags):
- self._ignored_depth += 1
- return
- if self._data:
- self._flush()
- new_namespaces = []
- self._declared_ns_stack.append(new_namespaces)
- if self._qname_aware_tags is not None and tag in self._qname_aware_tags:
- # Need to parse text first to see if it requires a prefix declaration.
- self._pending_start = (tag, attrs, new_namespaces)
- return
- self._start(tag, attrs, new_namespaces)
- def _start(self, tag, attrs, new_namespaces, qname_text=None):
- if self._exclude_attrs is not None and attrs:
- attrs = {k: v for k, v in attrs.items() if k not in self._exclude_attrs}
- qnames = {tag, *attrs}
- resolved_names = {}
- # Resolve prefixes in attribute and tag text.
- if qname_text is not None:
- qname = resolved_names[qname_text] = self._resolve_prefix_name(qname_text)
- qnames.add(qname)
- if self._find_qname_aware_attrs is not None and attrs:
- qattrs = self._find_qname_aware_attrs(attrs)
- if qattrs:
- for attr_name in qattrs:
- value = attrs[attr_name]
- if _looks_like_prefix_name(value):
- qname = resolved_names[value] = self._resolve_prefix_name(value)
- qnames.add(qname)
- else:
- qattrs = None
- else:
- qattrs = None
- # Assign prefixes in lexicographical order of used URIs.
- parse_qname = self._qname
- parsed_qnames = {n: parse_qname(n) for n in sorted(
- qnames, key=lambda n: n.split('}', 1))}
- # Write namespace declarations in prefix order ...
- if new_namespaces:
- attr_list = [
- ('xmlns:' + prefix if prefix else 'xmlns', uri)
- for uri, prefix in new_namespaces
- ]
- attr_list.sort()
- else:
- # almost always empty
- attr_list = []
- # ... followed by attributes in URI+name order
- if attrs:
- for k, v in sorted(attrs.items()):
- if qattrs is not None and k in qattrs and v in resolved_names:
- v = parsed_qnames[resolved_names[v]][0]
- attr_qname, attr_name, uri = parsed_qnames[k]
- # No prefix for attributes in default ('') namespace.
- attr_list.append((attr_qname if uri else attr_name, v))
- # Honour xml:space attributes.
- space_behaviour = attrs.get('{http://www.w3.org/XML/1998/namespace}space')
- self._preserve_space.append(
- space_behaviour == 'preserve' if space_behaviour
- else self._preserve_space[-1])
- # Write the tag.
- write = self._write
- write('<' + parsed_qnames[tag][0])
- if attr_list:
- write(''.join([f' {k}="{_escape_attrib_c14n(v)}"' for k, v in attr_list]))
- write('>')
- # Write the resolved qname text content.
- if qname_text is not None:
- write(_escape_cdata_c14n(parsed_qnames[resolved_names[qname_text]][0]))
- self._root_seen = True
- self._ns_stack.append([])
- def end(self, tag):
- if self._ignored_depth:
- self._ignored_depth -= 1
- return
- if self._data:
- self._flush()
- self._write(f'</{self._qname(tag)[0]}>')
- self._preserve_space.pop()
- self._root_done = len(self._preserve_space) == 1
- self._declared_ns_stack.pop()
- self._ns_stack.pop()
- def comment(self, text):
- if not self._with_comments:
- return
- if self._ignored_depth:
- return
- if self._root_done:
- self._write('\n')
- elif self._root_seen and self._data:
- self._flush()
- self._write(f'<!--{_escape_cdata_c14n(text)}-->')
- if not self._root_seen:
- self._write('\n')
- def pi(self, target, data):
- if self._ignored_depth:
- return
- if self._root_done:
- self._write('\n')
- elif self._root_seen and self._data:
- self._flush()
- self._write(
- f'<?{target} {_escape_cdata_c14n(data)}?>' if data else f'<?{target}?>')
- if not self._root_seen:
- self._write('\n')
- def _escape_cdata_c14n(text):
- # 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('>', '>')
- if '\r' in text:
- text = text.replace('\r', '
')
- return text
- except (TypeError, AttributeError):
- _raise_serialization_error(text)
- def _escape_attrib_c14n(text):
- # escape attribute value
- try:
- if '&' in text:
- text = text.replace('&', '&')
- if '<' in text:
- text = text.replace('<', '<')
- if '"' in text:
- text = text.replace('"', '"')
- if '\t' in text:
- text = text.replace('\t', '	')
- if '\n' in text:
- text = text.replace('\n', '
')
- if '\r' in text:
- text = text.replace('\r', '
')
- return text
- except (TypeError, AttributeError):
- _raise_serialization_error(text)
- # --------------------------------------------------------------------
- # 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, _set_factories
- from _elementtree import *
- from _elementtree import _set_factories
- except ImportError:
- pass
- else:
- _set_factories(Comment, ProcessingInstruction)
|