diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 7948b2ed78f4d0d..9b152b08346af40 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -1277,6 +1277,28 @@ QName Objects ^^^^^^^^^^^^^ +.. class:: CDATA(text) + + A subclass of :class:`str` for character data + which is serialized as a CDATA section. + It can be used as the text or the tail of an element:: + + elem.text = CDATA(' & unescaped') + + The content of a CDATA section is character data: + it is not escaped when serialized, and it is a part of the inner text + returned by :meth:`Element.itertext` and by the ``"text"`` + serialization method. + ``"]]>"`` cannot occur in a CDATA section, + so a content which contains it is split into several sections. + + Note that CDATA sections in the input are parsed as ordinary character + data by default, and a :class:`CDATA` string is only created if the + parser target preserves them; see :class:`TreeBuilder`. + + .. versionadded:: next + + .. class:: QName(text_or_uri, tag=None) QName wrapper. This can be used to wrap a QName attribute value, in order @@ -1295,7 +1317,8 @@ TreeBuilder Objects .. class:: TreeBuilder(element_factory=None, *, comment_factory=None, \ - pi_factory=None, insert_comments=False, insert_pis=False) + pi_factory=None, insert_comments=False, \ + insert_pis=False, insert_cdata=False) Generic element structure builder. This builder converts a sequence of start, data, end, comment and pi method calls to a well-formed element @@ -1313,6 +1336,20 @@ TreeBuilder Objects comments/pis will be inserted into the tree if they appear within the root element (but not outside of it). + When *insert_cdata* is true, the content of a CDATA section is added to + the tree as a :class:`CDATA` string, so that the section is preserved + when the tree is serialized. + It becomes the text or the tail of an element if that is not set yet, + and otherwise a new element with the tag ``None`` is created to hold it. + A section does not share the place with what follows it: if character data + or another section follows it, it is moved to such an element, and the + character data becomes the tail of that element. + When *insert_cdata* is false (the default), + the content is added as ordinary character data. + + .. versionchanged:: next + Added the *insert_cdata* argument. + .. method:: close() Flushes the builder buffers, and returns the toplevel document @@ -1330,6 +1367,27 @@ TreeBuilder Objects closed element. + .. method:: start_cdata() + + Begins a CDATA section. + The text added by :meth:`data` until the matching :meth:`end_cdata` + call is the content of the section. + + .. versionadded:: next + + + .. method:: end_cdata() + + Ends a CDATA section. + If *insert_cdata* is true, adds the collected content to the tree as a + :class:`CDATA` string, and returns the element created to hold it, or + ``None`` if it was added as the text or the tail of an existing element. + If *insert_cdata* is false, returns ``None`` and the collected content + is left as ordinary character data. + + .. versionadded:: next + + .. method:: start(tag, attrs) Opens a new element. *tag* is the element name. *attrs* is a dictionary diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 3262acd87d6d49f..60cf9123eaea0b1 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -660,6 +660,13 @@ xml rather than defaulted from the DTD. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* Add :class:`~xml.etree.ElementTree.CDATA` to :mod:`xml.etree.ElementTree`, + a :class:`str` subclass for character data which is serialized as a CDATA + section. :class:`~xml.etree.ElementTree.TreeBuilder` supports the + *insert_cdata* argument, which makes it preserve CDATA sections of the + parsed document. + (Contributed by Serhiy Storchaka in :gh:`81055`.) + zipfile ------- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 4fd6c618fb74404..cc62e6d17fbb414 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1842,6 +1842,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(initval)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(inner_size)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(input)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(insert_cdata)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(insert_comments)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(insert_pis)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(instructions)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 5b35c53e0aa03b1..a060a02f91f1e61 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -565,6 +565,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(initval) STRUCT_FOR_ID(inner_size) STRUCT_FOR_ID(input) + STRUCT_FOR_ID(insert_cdata) STRUCT_FOR_ID(insert_comments) STRUCT_FOR_ID(insert_pis) STRUCT_FOR_ID(instructions) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index c80925f020186ba..65cc7aa462fb39e 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1840,6 +1840,7 @@ extern "C" { INIT_ID(initval), \ INIT_ID(inner_size), \ INIT_ID(input), \ + INIT_ID(insert_cdata), \ INIT_ID(insert_comments), \ INIT_ID(insert_pis), \ INIT_ID(instructions), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index b30cfc678de1cd7..08a2f7999c70e09 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2040,6 +2040,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(insert_cdata); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(insert_comments); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Include/pyexpat.h b/Include/pyexpat.h index a676e16a7a457ea..4d120d0401bdce3 100644 --- a/Include/pyexpat.h +++ b/Include/pyexpat.h @@ -65,6 +65,9 @@ struct PyExpat_CAPI /* might be NULL for expat < 2.8.0 */ XML_Bool (*SetHashSalt16Bytes)( XML_Parser parser, const uint8_t entropy[16]); + void (*SetCdataSectionHandler)( + XML_Parser parser, XML_StartCdataSectionHandler start, + XML_EndCdataSectionHandler end); /* always add new stuff to the end! */ }; diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index f9ff8c4c3541eda..13529a5f84a015a 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -1464,6 +1464,55 @@ def test_comment_serialization(self): # no comments in text serialization self.assertEqual(ET.tostring(comm, method='text'), b'') + def test_cdata_serialization(self): + elem = ET.Element('root') + elem.text = ET.CDATA(' & ham') + # the content of a CDATA section is not escaped + self.assertEqual(ET.tostring(elem), + b' & ham]]>') + # but it is character data + self.assertEqual(ET.tostring(elem, method='text'), b' & ham') + # an empty CDATA section is preserved + elem.text = ET.CDATA('') + self.assertEqual(ET.tostring(elem), b'') + empty = ET.Element('root') + sub = ET.SubElement(empty, 'sub') + sub.tail = ET.CDATA('') + self.assertEqual(ET.tostring(empty), + b'') + # "]]>" cannot occur in a CDATA section, it is split in two + elem.text = ET.CDATA('a]]>b') + self.assertEqual(ET.tostring(elem), + b'b]]>') + + def test_cdata_as_tail(self): + elem = ET.XML('before') + elem[0].tail = ET.CDATA(' & ham') + self.assertEqual(ET.tostring(elem), + b'before & ham]]>') + self.assertEqual(ET.tostring(elem, method='text'), + b'before & ham') + # the written form is parsed back to the same text + self.assertEqual(''.join(ET.fromstring(ET.tostring(elem)).itertext()), + 'before & ham') + + def test_cdata_is_str(self): + cdata = ET.CDATA('text') + self.assertIsInstance(cdata, str) + self.assertEqual(cdata, 'text') + # an ordinary string is not serialized as a CDATA section + elem = ET.Element('root') + elem.text = str(cdata) + self.assertEqual(ET.tostring(elem), b'text') + + def test_cdata_subclass(self): + class MyCDATA(ET.CDATA): + pass + + elem = ET.Element('root') + elem.text = MyCDATA('') + self.assertEqual(ET.tostring(elem), b']]>') + def test_processinginstruction_serialization(self): # Test ProcessingInstruction directly @@ -4058,6 +4107,176 @@ def test_treebuilder_pi(self): self.assertEqual(b.pi('target'), (len('target'), None)) self.assertEqual(b.pi('pitarget', ' text '), (len('pitarget'), ' text ')) + def test_treebuilder_cdata(self): + b = ET.TreeBuilder() + # nothing is created unless insert_cdata is true + self.assertIsNone(b.start_cdata()) + self.assertIsNone(b.end_cdata()) + + b = ET.TreeBuilder(insert_cdata=True) + b.start('a', {}) + b.data('before') + b.start_cdata() + b.data('a < b') + elem = b.end_cdata() + self.assertIsNone(elem.tag) + self.assertEqual(elem.text, 'a < b') + self.assertIsInstance(elem.text, ET.CDATA) + b.data('after') + b.end('a') + a = b.close() + self.assertEqual(ET.tostring(a), + b'beforeafter') + + def test_parse_cdata_alternating(self): + parser = ET.XMLParser(target=ET.TreeBuilder(insert_cdata=True)) + xml = 't1t2' + parser.feed(xml) + a = parser.close() + # each section is the text of its element, the text which follows + # it is the tail + self.assertEqual(summarize_list(a), [None, None]) + self.assertIsNone(a.text) + self.assertEqual(a[0].text, 'x') + self.assertIsInstance(a[0].text, ET.CDATA) + self.assertEqual(a[0].tail, 't1') + self.assertEqual(a[1].text, 'y') + self.assertIsInstance(a[1].text, ET.CDATA) + self.assertEqual(a[1].tail, 't2') + self.assertEqual(ET.tostring(a, encoding='unicode'), xml) + + def test_parse_cdata(self): + xml = 'beforeafter' + # by default the content of a CDATA section is ordinary text + a = ET.fromstring(xml) + self.assertEqual(ET.tostring(a), + b'beforea < bafterdeep') + + parser = ET.XMLParser(target=ET.TreeBuilder(insert_cdata=True)) + parser.feed(xml) + a = parser.close() + self.assertEqual(summarize_list(a), [None, 'b']) + self.assertEqual(a.text, 'before') + self.assertEqual(a[0].text, 'a < b') + self.assertIsInstance(a[0].text, ET.CDATA) + self.assertEqual(a[0].tail, 'after') + # no element is needed for the section which is the whole content + self.assertEqual(summarize_list(a[1]), []) + self.assertEqual(a[1].text, 'deep') + self.assertIsInstance(a[1].text, ET.CDATA) + # the content of a CDATA section is a part of the inner text + self.assertEqual(''.join(a.itertext()), 'beforea < bafterdeep') + # the tree is serialized back to the source + self.assertEqual(ET.tostring(a, encoding='unicode'), xml) + + def test_parse_empty_cdata(self): + parser = ET.XMLParser(target=ET.TreeBuilder(insert_cdata=True)) + parser.feed('') + a = parser.close() + # no element is needed, the content is the text of the parent + self.assertEqual(summarize_list(a), []) + self.assertEqual(a.text, '') + self.assertIsInstance(a.text, ET.CDATA) + # an empty section is written back + self.assertEqual(ET.tostring(a), b'') + + def test_parse_cdata_merged(self): + # No element is created if the content can be the text of the parent + # or the tail of the preceding sibling. + def parse(xml): + parser = ET.XMLParser(target=ET.TreeBuilder(insert_cdata=True)) + parser.feed(xml) + return parser.close() + + a = parse('') + self.assertEqual(summarize_list(a), []) + self.assertEqual(a.text, 'x') + self.assertIsInstance(a.text, ET.CDATA) + + a = parse('') + self.assertEqual(summarize_list(a), ['b']) + self.assertEqual(a[0].tail, 'x') + self.assertIsInstance(a[0].tail, ET.CDATA) + + # a section does not share the place with what follows it + a = parse('') + self.assertEqual(summarize_list(a), [None, None]) + self.assertIsNone(a.text) + self.assertEqual(a[0].text, 'x') + self.assertEqual(a[1].text, 'y') + + a = parse('') + self.assertEqual(summarize_list(a), ['b', None, None]) + self.assertIsNone(a[0].tail) + self.assertEqual(a[1].text, 'x') + self.assertEqual(a[2].text, 'y') + + # the element is needed if the preceding text is not empty + a = parse('text') + self.assertEqual(summarize_list(a), [None]) + a = parse('t') + self.assertEqual(summarize_list(a), ['b', None]) + # if text follows the section, it is moved to an element + # and the text becomes its tail + a = parse('tail') + self.assertEqual(summarize_list(a), [None]) + self.assertIsNone(a.text) + self.assertEqual(a[0].text, 'x') + self.assertIsInstance(a[0].text, ET.CDATA) + self.assertEqual(a[0].tail, 'tail') + + @support.subTests('xml', ( + '', + 'tail', + 'text', + '', + 't', + '', + '', + '', + '', + )) + def test_parse_cdata_roundtrip(self, xml): + parser = ET.XMLParser(target=ET.TreeBuilder(insert_cdata=True)) + parser.feed(xml) + self.assertEqual(ET.tostring(parser.close(), encoding='unicode'), xml) + + def test_parse_cdata_subclass(self): + class TreeBuilderSubclass(ET.TreeBuilder): + pass + + xml = 'texttail' + parser = ET.XMLParser(target=TreeBuilderSubclass(insert_cdata=True)) + parser.feed(xml) + a = parser.close() + self.assertEqual(a.text, 'text') + self.assertEqual(a[0].text, 'a < b') + self.assertEqual(a[0].tail, 'tail') + + def test_parse_cdata_custom_target(self): + events = [] + class Target: + def start(self, tag, attrib): + events.append(('start', tag)) + def end(self, tag): + events.append(('end', tag)) + def data(self, data): + events.append(('data', data)) + def start_cdata(self): + events.append(('start_cdata',)) + def end_cdata(self): + events.append(('end_cdata',)) + def close(self): + return events + + parser = ET.XMLParser(target=Target()) + parser.feed('texttail') + self.assertEqual(parser.close(), [ + ('start', 'a'), ('data', 'text'), + ('start_cdata',), ('data', 'a < b'), ('end_cdata',), + ('data', 'tail'), ('end', 'a'), + ]) + def test_late_tail(self): # Issue #37399: The tail of an ignored comment could overwrite the text before it. class TreeBuilderSubclass(ET.TreeBuilder): @@ -5140,9 +5359,9 @@ def cleanup(): unittest.addModuleCleanup(setattr, ElementPath, "_cache", path_cache) ElementPath._cache = path_cache.copy() - # Align the Comment/PI factories. + # Align the Comment/PI factories and the CDATA type. if hasattr(ET, '_set_factories'): - old_factories = ET._set_factories(ET.Comment, ET.PI) + old_factories = ET._set_factories(ET.Comment, ET.PI, ET.CDATA) unittest.addModuleCleanup(ET._set_factories, *old_factories) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index bed8c27df5a3845..8ef99ab0f56adea 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -72,6 +72,7 @@ __all__ = [ # public symbols + "CDATA", "Comment", "dump", "Element", "ElementTree", @@ -434,6 +435,19 @@ def SubElement(parent, tag, /, attrib={}, **extra): return element +class CDATA(str): + """Character data which is serialized as a CDATA section. + + It is a string, and can be used as the text or the tail of an element: + + elem.text = CDATA(" & unescaped") + + The parser creates such strings for the content of CDATA sections + if the CDATA sections are preserved. + """ + __slots__ = () + + def Comment(text=None): """Comment element factory. @@ -877,6 +891,7 @@ def _serialize_xml(write, elem, qnames, namespaces, short_empty_elements, **kwargs): tag = elem.tag text = elem.text + has_text = text or isinstance(text, CDATA) if tag is Comment: write("" % text) elif tag is ProcessingInstruction: @@ -884,7 +899,7 @@ def _serialize_xml(write, elem, qnames, namespaces, else: tag = qnames[tag] if tag is None: - if text: + if has_text: write(_escape_cdata(text)) for e in elem: _serialize_xml(write, e, qnames, None, @@ -910,9 +925,9 @@ def _serialize_xml(write, elem, qnames, namespaces, else: v = _escape_attrib(v) write(" %s=\"%s\"" % (qnames[k], v)) - if text or len(elem) or not short_empty_elements: + if has_text or len(elem) or not short_empty_elements: write(">") - if text: + if has_text: write(_escape_cdata(text)) for e in elem: _serialize_xml(write, e, qnames, None, @@ -920,8 +935,9 @@ def _serialize_xml(write, elem, qnames, namespaces, write("") else: write(" />") - if elem.tail: - write(_escape_cdata(elem.tail)) + tail = elem.tail + if tail or isinstance(tail, CDATA): + write(_escape_cdata(tail)) _CDATA_CONTENT_ELEMENTS = {"script", "style", "xmp", "iframe", "noembed", "noframes", "plaintext"} @@ -933,6 +949,7 @@ def _serialize_xml(write, elem, qnames, namespaces, def _serialize_html(write, elem, qnames, namespaces, **kwargs): tag = elem.tag text = elem.text + has_text = text or isinstance(text, CDATA) if tag is Comment: write("" % text) elif tag is ProcessingInstruction: @@ -940,7 +957,7 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs): else: tag = qnames[tag] if tag is None: - if text: + if has_text: write(_escape_cdata(text)) for e in elem: _serialize_html(write, e, qnames, None) @@ -971,7 +988,7 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs): write(" %s=\"%s\"" % (k, v)) write(">") ltag = tag.lower() - if text: + if has_text: if ltag in _CDATA_CONTENT_ELEMENTS: write(text) else: @@ -980,8 +997,9 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs): _serialize_html(write, e, qnames, None) if ltag not in HTML_EMPTY: write("") - if elem.tail: - write(_escape_cdata(elem.tail)) + tail = elem.tail + if tail or isinstance(tail, CDATA): + write(_escape_cdata(tail)) def _serialize_text(write, elem): for part in elem.itertext(): @@ -1035,8 +1053,17 @@ def _raise_serialization_error(text): "cannot serialize %r (type %s)" % (text, type(text).__name__) ) +def _cdata_section(text): + # write character data as a CDATA section + if "]]>" in text: + # a CDATA section cannot contain "]]>", split it in two + text = text.replace("]]>", "]]]]>") + return "" + def _escape_cdata(text): # escape character data + if isinstance(text, CDATA): + return _cdata_section(text) try: # it's worth avoiding do-nothing calls for strings that are # shorter than 500 characters, or so. assume that's, by far, @@ -1443,10 +1470,15 @@ class TreeBuilder: *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. + + If *insert_cdata* is false (the default), the content of CDATA sections + is added to the tree as ordinary text. Otherwise it is added as a + :class:`CDATA` string, in a separate element with the tag ``None``. """ def __init__(self, element_factory=None, *, comment_factory=None, pi_factory=None, - insert_comments=False, insert_pis=False): + insert_comments=False, insert_pis=False, + insert_cdata=False): self._data = [] # data collector self._elem = [] # element stack self._last = None # last element @@ -1460,6 +1492,7 @@ def __init__(self, element_factory=None, *, pi_factory = ProcessingInstruction self._pi_factory = pi_factory self.insert_pis = insert_pis + self.insert_cdata = insert_cdata if element_factory is None: element_factory = Element self._factory = element_factory @@ -1473,15 +1506,49 @@ def close(self): 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._add_text("".join(self._data)) self._data = [] + def _add_text(self, text): + # Add character data at the current position: as the text or the tail + # of the last element if it is not set yet, and otherwise in a new + # element with the tag None (which only happens with CDATA sections). + last = self._last + if self._tail: + value = last.tail + else: + value = last.text + if value is None: + if self._tail: + last.tail = text + else: + last.text = text + return None + assert self.insert_cdata, "internal error (text)" + if isinstance(value, CDATA): + # a CDATA section does not share the place with what follows it, + # so move it into its own element + if self._tail: + last.tail = None + else: + last.text = None + last = self._new_text_element(last, value) + if not isinstance(text, CDATA): + last.tail = text + return last + return self._new_text_element(last, text) + + def _new_text_element(self, last, text): + elem = self._factory(None, {}) + elem.text = text + if self._tail: + self._elem[-1].append(elem) + else: + last.append(elem) + self._last = elem + self._tail = 1 + return elem + def data(self, data): """Add text to current element.""" self._data.append(data) @@ -1534,6 +1601,30 @@ def pi(self, target, text=None): return self._handle_single( self._pi_factory, self.insert_pis, target, text) + def start_cdata(self): + """Begin a CDATA section. + + The data collected until the matching end_cdata() call is the + content of the section. + """ + if self.insert_cdata: + self._flush() + + def end_cdata(self): + """End a CDATA section and add its content to the tree. + + The content is added as a CDATA string: as the text of the current + element or the tail of the last one if they are not set yet, and + otherwise in a new element with the tag ``None``, which is returned. + """ + if not self.insert_cdata: + return None + text = CDATA("".join(self._data)) + self._data = [] + if self._last is None: + return None + return self._add_text(text) + def _handle_single(self, factory, insert, *args): elem = factory(*args) if insert: @@ -1591,6 +1682,10 @@ def __init__(self, *, target=None, encoding=None): parser.CommentHandler = target.comment if hasattr(target, 'pi'): parser.ProcessingInstructionHandler = target.pi + if hasattr(target, 'start_cdata'): + parser.StartCdataSectionHandler = target.start_cdata + if hasattr(target, 'end_cdata'): + parser.EndCdataSectionHandler = target.end_cdata # Configure pyexpat: buffering, new-style attribute handling. parser.buffer_text = 1 parser.ordered_attributes = 1 @@ -2132,7 +2227,7 @@ def _escape_attrib_c14n(text): except ImportError: pass else: - _set_factories(Comment, ProcessingInstruction) + _set_factories(Comment, ProcessingInstruction, CDATA) # -------------------------------------------------------------------- diff --git a/Misc/NEWS.d/next/Library/2026-09-04-10-15-00.gh-issue-81055.Vt7pQ2.rst b/Misc/NEWS.d/next/Library/2026-09-04-10-15-00.gh-issue-81055.Vt7pQ2.rst new file mode 100644 index 000000000000000..7a7934c06f20976 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-04-10-15-00.gh-issue-81055.Vt7pQ2.rst @@ -0,0 +1,6 @@ +Add :class:`xml.etree.ElementTree.CDATA`, a :class:`str` subclass for +character data which is serialized as a CDATA section. +:class:`~xml.etree.ElementTree.TreeBuilder` now supports the *insert_cdata* +argument: if it is true, the content of a CDATA section is added to the tree +as a :class:`~xml.etree.ElementTree.CDATA` string, so that the section is +preserved when the tree is serialized. diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index f6dedeed981c40d..c4e9420d077b4d5 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -86,6 +86,7 @@ typedef struct { PyObject *elementpath_obj; PyObject *comment_factory; PyObject *pi_factory; + PyObject *cdata_type; /* Interned strings */ PyObject *str_text; PyObject *str_tail; @@ -143,6 +144,7 @@ elementtree_clear(PyObject *m) Py_CLEAR(st->elementpath_obj); Py_CLEAR(st->comment_factory); Py_CLEAR(st->pi_factory); + Py_CLEAR(st->cdata_type); // Interned strings Py_CLEAR(st->str_append); @@ -174,6 +176,7 @@ elementtree_traverse(PyObject *m, visitproc visit, void *arg) Py_VISIT(st->elementpath_obj); Py_VISIT(st->comment_factory); Py_VISIT(st->pi_factory); + Py_VISIT(st->cdata_type); // Heap types Py_VISIT(st->Element_Type); @@ -2514,6 +2517,7 @@ typedef struct { char insert_comments; char insert_pis; + char insert_cdata; elementtreestate *state; } TreeBuilderObject; @@ -2549,7 +2553,7 @@ treebuilder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) t->start_event_obj = t->end_event_obj = NULL; t->start_ns_event_obj = t->end_ns_event_obj = NULL; t->comment_event_obj = t->pi_event_obj = NULL; - t->insert_comments = t->insert_pis = 0; + t->insert_comments = t->insert_pis = t->insert_cdata = 0; t->state = get_elementtree_state_by_type(type); } return (PyObject *)t; @@ -2564,6 +2568,7 @@ _elementtree.TreeBuilder.__init__ pi_factory: object = None insert_comments: bool = False insert_pis: bool = False + insert_cdata: bool = False Generic element structure builder. @@ -2583,6 +2588,9 @@ 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. + +If *insert_cdata* is false (the default), the content of CDATA +sections is added to the tree as ordinary text. [clinic start generated code]*/ static int @@ -2590,8 +2598,9 @@ _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, PyObject *element_factory, PyObject *comment_factory, PyObject *pi_factory, - int insert_comments, int insert_pis) -/*[clinic end generated code: output=8571d4dcadfdf952 input=24fb5a482d93f8e4]*/ + int insert_comments, int insert_pis, + int insert_cdata) +/*[clinic end generated code: output=a6defd6f17e8a21c input=a2083a977d31af00]*/ { if (element_factory != Py_None) { Py_XSETREF(self->element_factory, Py_NewRef(element_factory)); @@ -2623,6 +2632,8 @@ _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, self->insert_pis = 0; } + self->insert_cdata = insert_cdata; + return 0; } @@ -2692,17 +2703,18 @@ _elementtree._set_factories comment_factory: object pi_factory: object + cdata_type: object / -Change the factories used to create comments and processing instructions. +Change the factories used to create comments, PIs and CDATA strings. For internal use only. [clinic start generated code]*/ static PyObject * _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, - PyObject *pi_factory) -/*[clinic end generated code: output=813b408adee26535 input=0f415cb6b821f768]*/ + PyObject *pi_factory, PyObject *cdata_type) +/*[clinic end generated code: output=496e6a88be282ed6 input=bf602f78b2a3ea54]*/ { elementtreestate *st = get_elementtree_state(module); PyObject *old; @@ -2718,9 +2730,19 @@ _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, return NULL; } - old = _PyTuple_FromPair( + if (!PyCallable_Check(cdata_type) && cdata_type != Py_None) { + PyErr_Format(PyExc_TypeError, "CDATA type must be callable, not %.100s", + Py_TYPE(cdata_type)->tp_name); + return NULL; + } + + old = Py_BuildValue("OOO", st->comment_factory ? st->comment_factory : Py_None, - st->pi_factory ? st->pi_factory : Py_None); + st->pi_factory ? st->pi_factory : Py_None, + st->cdata_type ? st->cdata_type : Py_None); + if (old == NULL) { + return NULL; + } if (comment_factory == Py_None) { Py_CLEAR(st->comment_factory); @@ -2732,6 +2754,11 @@ _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, } else { Py_XSETREF(st->pi_factory, Py_NewRef(pi_factory)); } + if (cdata_type == Py_None) { + Py_CLEAR(st->cdata_type); + } else { + Py_XSETREF(st->cdata_type, Py_NewRef(cdata_type)); + } return old; } @@ -2791,12 +2818,67 @@ treebuilder_extend_element_text_or_tail(elementtreestate *st, PyObject *element, } } +/* Create an element with the tag None holding the given text, add it to the + tree, and make it the element whose tail is filled next. */ +LOCAL(PyObject*) +treebuilder_new_text_element(TreeBuilderObject* self, PyObject* text); + +/* Return the text or the tail which the collected data is added to, + a borrowed reference, or NULL if it is not an exact Element. */ +LOCAL(PyObject*) +treebuilder_current_text(TreeBuilderObject* self) +{ + elementtreestate *st = self->state; + PyObject *element = self->last_for_tail ? self->last_for_tail : self->last; + if (!Element_CheckExact(st, element)) { + return NULL; + } + ElementObject *elem = (ElementObject *) element; + return JOIN_OBJ(self->last_for_tail ? elem->tail : elem->text); +} + +/* If the text or the tail which the data is added to is a CDATA string, + move it into a separate element: a section does not share the place + with what follows it. */ +LOCAL(int) +treebuilder_move_cdata(TreeBuilderObject* self) +{ + elementtreestate *st = self->state; + if (!self->insert_cdata || !st->cdata_type) { + return 0; + } + PyObject *text = treebuilder_current_text(self); + if (text == NULL || text == Py_None || !PyType_Check(st->cdata_type) + || !PyObject_TypeCheck(text, (PyTypeObject *) st->cdata_type)) { + return 0; + } + PyObject *element = self->last_for_tail ? self->last_for_tail : self->last; + ElementObject *elem = (ElementObject *) element; + PyObject *cdata = Py_NewRef(text); + if (self->last_for_tail) { + Py_SETREF(elem->tail, Py_NewRef(Py_None)); + } + else { + Py_SETREF(elem->text, Py_NewRef(Py_None)); + } + PyObject *node = treebuilder_new_text_element(self, cdata); + Py_DECREF(cdata); + if (node == NULL) { + return -1; + } + Py_DECREF(node); + return 0; +} + LOCAL(int) treebuilder_flush_data(TreeBuilderObject* self) { if (!self->data) { return 0; } + if (treebuilder_move_cdata(self) < 0) { + return -1; + } elementtreestate *st = self->state; if (!self->last_for_tail) { PyObject *element = self->last; @@ -2994,6 +3076,102 @@ treebuilder_handle_end(TreeBuilderObject* self, PyObject* tag) return this; } +LOCAL(PyObject*) +treebuilder_new_text_element(TreeBuilderObject* self, PyObject* text) +{ + elementtreestate *st = self->state; + PyObject *node; + + if (!self->element_factory) { + node = create_new_element(st, Py_None, NULL); + } + else { + PyObject *attrib = PyDict_New(); + if (!attrib) { + return NULL; + } + node = PyObject_CallFunctionObjArgs(self->element_factory, + Py_None, attrib, NULL); + Py_DECREF(attrib); + } + if (node == NULL) { + return NULL; + } + if (PyObject_SetAttr(node, st->str_text, text) < 0) { + Py_DECREF(node); + return NULL; + } + if (self->this != Py_None) { + if (treebuilder_add_subelement(st, self->this, node) < 0) { + Py_DECREF(node); + return NULL; + } + } + Py_XSETREF(self->last_for_tail, Py_NewRef(node)); + return node; +} + +LOCAL(int) +treebuilder_handle_cdata_start(TreeBuilderObject* self) +{ + if (!self->insert_cdata) { + return 0; + } + /* the text before the section belongs to the preceding node */ + return treebuilder_flush_data(self); +} + +LOCAL(PyObject*) +treebuilder_handle_cdata_end(TreeBuilderObject* self) +{ + elementtreestate *st = self->state; + PyObject *text, *cdata, *slot, *node; + + if (!self->insert_cdata || !st->cdata_type) { + Py_RETURN_NONE; + } + + if (self->data) { + text = PyList_CheckExact(self->data) ? list_join(self->data) + : Py_NewRef(self->data); + Py_CLEAR(self->data); + } + else { + text = Py_GetConstant(Py_CONSTANT_EMPTY_STR); + } + if (!text) { + return NULL; + } + cdata = PyObject_CallOneArg(st->cdata_type, text); + Py_DECREF(text); + if (!cdata) { + return NULL; + } + + /* use the text or the tail of the last element if it is not set yet */ + slot = treebuilder_current_text(self); + if (slot == Py_None) { + PyObject *element = + self->last_for_tail ? self->last_for_tail : self->last; + ElementObject *elem = (ElementObject *) element; + if (self->last_for_tail) { + Py_SETREF(elem->tail, cdata); + } + else { + Py_SETREF(elem->text, cdata); + } + Py_RETURN_NONE; + } + /* a section does not share the place with what follows it */ + if (treebuilder_move_cdata(self) < 0) { + Py_DECREF(cdata); + return NULL; + } + node = treebuilder_new_text_element(self, cdata); + Py_DECREF(cdata); + return node; +} + LOCAL(PyObject*) treebuilder_handle_comment(TreeBuilderObject* self, PyObject* text) { @@ -3145,6 +3323,38 @@ _elementtree_TreeBuilder_end_impl(TreeBuilderObject *self, PyObject *tag) return treebuilder_handle_end(self, tag); } +/*[clinic input] +_elementtree.TreeBuilder.start_cdata + +Begin a CDATA section. + +The data collected until the matching end_cdata() call is the content +of the section. +[clinic start generated code]*/ + +static PyObject * +_elementtree_TreeBuilder_start_cdata_impl(TreeBuilderObject *self) +/*[clinic end generated code: output=433004da9adec83c input=e5092094671a95ee]*/ +{ + if (treebuilder_handle_cdata_start(self) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +/*[clinic input] +_elementtree.TreeBuilder.end_cdata + +End a CDATA section and add its content to the tree. +[clinic start generated code]*/ + +static PyObject * +_elementtree_TreeBuilder_end_cdata_impl(TreeBuilderObject *self) +/*[clinic end generated code: output=7795f7e05cd36223 input=75b233a47813c77f]*/ +{ + return treebuilder_handle_cdata_end(self); +} + /*[clinic input] _elementtree.TreeBuilder.comment @@ -3260,6 +3470,8 @@ typedef struct { PyObject *handle_comment; PyObject *handle_pi; + PyObject *handle_cdata_start; + PyObject *handle_cdata_end; PyObject *handle_doctype; PyObject *handle_close; @@ -3699,6 +3911,46 @@ expat_comment_handler(void *op, const XML_Char *comment_in) } } +static void +expat_start_cdata_handler(void *op) +{ + XMLParserObject *self = XMLParserObject_CAST(op); + + if (PyErr_Occurred()) + return; + + elementtreestate *st = self->state; + if (TreeBuilder_CheckExact(st, self->target)) { + /* shortcut */ + (void)treebuilder_handle_cdata_start((TreeBuilderObject*) self->target); + } + else if (self->handle_cdata_start) { + PyObject *res = PyObject_CallNoArgs(self->handle_cdata_start); + Py_XDECREF(res); + } +} + +static void +expat_end_cdata_handler(void *op) +{ + XMLParserObject *self = XMLParserObject_CAST(op); + PyObject *res; + + if (PyErr_Occurred()) + return; + + elementtreestate *st = self->state; + if (TreeBuilder_CheckExact(st, self->target)) { + /* shortcut */ + res = treebuilder_handle_cdata_end((TreeBuilderObject*) self->target); + Py_XDECREF(res); + } + else if (self->handle_cdata_end) { + res = PyObject_CallNoArgs(self->handle_cdata_end); + Py_XDECREF(res); + } +} + static void expat_start_doctype_handler(void *op, const XML_Char *doctype_name, @@ -3821,6 +4073,7 @@ xmlparser_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->handle_start_ns = self->handle_end_ns = NULL; self->handle_start = self->handle_data = self->handle_end = NULL; self->handle_comment = self->handle_pi = self->handle_close = NULL; + self->handle_cdata_start = self->handle_cdata_end = NULL; self->handle_doctype = NULL; self->elementtree_module = PyType_GetModuleByDef(type, &elementtreemodule); assert(self->elementtree_module != NULL); @@ -3923,6 +4176,14 @@ _elementtree_XMLParser___init___impl(XMLParserObject *self, PyObject *target, if (ignore_attribute_error(self->handle_end)) { return -1; } + self->handle_cdata_start = PyObject_GetAttrString(target, "start_cdata"); + if (ignore_attribute_error(self->handle_cdata_start)) { + return -1; + } + self->handle_cdata_end = PyObject_GetAttrString(target, "end_cdata"); + if (ignore_attribute_error(self->handle_cdata_end)) { + return -1; + } self->handle_comment = PyObject_GetAttrString(target, "comment"); if (ignore_attribute_error(self->handle_comment)) { return -1; @@ -3971,6 +4232,13 @@ _elementtree_XMLParser___init___impl(XMLParserObject *self, PyObject *target, self->parser, (XML_ProcessingInstructionHandler) expat_pi_handler ); + if ((self->handle_cdata_start || self->handle_cdata_end) + && EXPAT(st, SetCdataSectionHandler) != NULL) + EXPAT(st, SetCdataSectionHandler)( + self->parser, + (XML_StartCdataSectionHandler) expat_start_cdata_handler, + (XML_EndCdataSectionHandler) expat_end_cdata_handler + ); EXPAT(st, SetStartDoctypeDeclHandler)( self->parser, (XML_StartDoctypeDeclHandler) expat_start_doctype_handler @@ -3991,6 +4259,8 @@ xmlparser_gc_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(self->handle_close); Py_VISIT(self->handle_pi); Py_VISIT(self->handle_comment); + Py_VISIT(self->handle_cdata_start); + Py_VISIT(self->handle_cdata_end); Py_VISIT(self->handle_end); Py_VISIT(self->handle_data); Py_VISIT(self->handle_start); @@ -4020,6 +4290,8 @@ xmlparser_gc_clear(PyObject *op) Py_CLEAR(self->handle_close); Py_CLEAR(self->handle_pi); Py_CLEAR(self->handle_comment); + Py_CLEAR(self->handle_cdata_start); + Py_CLEAR(self->handle_cdata_end); Py_CLEAR(self->handle_end); Py_CLEAR(self->handle_data); Py_CLEAR(self->handle_start); @@ -4552,6 +4824,8 @@ static PyMethodDef treebuilder_methods[] = { _ELEMENTTREE_TREEBUILDER_DATA_METHODDEF _ELEMENTTREE_TREEBUILDER_START_METHODDEF _ELEMENTTREE_TREEBUILDER_END_METHODDEF + _ELEMENTTREE_TREEBUILDER_START_CDATA_METHODDEF + _ELEMENTTREE_TREEBUILDER_END_CDATA_METHODDEF _ELEMENTTREE_TREEBUILDER_COMMENT_METHODDEF _ELEMENTTREE_TREEBUILDER_PI_METHODDEF _ELEMENTTREE_TREEBUILDER_CLOSE_METHODDEF diff --git a/Modules/clinic/_elementtree.c.h b/Modules/clinic/_elementtree.c.h index a39e738ec538b63..98cf9c79a2161c2 100644 --- a/Modules/clinic/_elementtree.c.h +++ b/Modules/clinic/_elementtree.c.h @@ -956,7 +956,8 @@ _elementtree_Element_set(PyObject *self, PyObject *const *args, Py_ssize_t nargs PyDoc_STRVAR(_elementtree_TreeBuilder___init____doc__, "TreeBuilder(element_factory=None, *, comment_factory=None,\n" -" pi_factory=None, insert_comments=False, insert_pis=False)\n" +" pi_factory=None, insert_comments=False, insert_pis=False,\n" +" insert_cdata=False)\n" "--\n" "\n" "Generic element structure builder.\n" @@ -976,14 +977,18 @@ PyDoc_STRVAR(_elementtree_TreeBuilder___init____doc__, "\n" "*pi_factory* is a factory to create processing instructions to be\n" "used instead of the standard factory. If *insert_pis* is false (the\n" -"default), processing instructions will not be inserted into the tree."); +"default), processing instructions will not be inserted into the tree.\n" +"\n" +"If *insert_cdata* is false (the default), the content of CDATA\n" +"sections is added to the tree as ordinary text."); static int _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, PyObject *element_factory, PyObject *comment_factory, PyObject *pi_factory, - int insert_comments, int insert_pis); + int insert_comments, int insert_pis, + int insert_cdata); static int _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwargs) @@ -991,7 +996,7 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar int return_value = -1; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 5 + #define NUM_KEYWORDS 6 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -1000,7 +1005,7 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(element_factory), &_Py_ID(comment_factory), &_Py_ID(pi_factory), &_Py_ID(insert_comments), &_Py_ID(insert_pis), }, + .ob_item = { &_Py_ID(element_factory), &_Py_ID(comment_factory), &_Py_ID(pi_factory), &_Py_ID(insert_comments), &_Py_ID(insert_pis), &_Py_ID(insert_cdata), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -1009,14 +1014,14 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"element_factory", "comment_factory", "pi_factory", "insert_comments", "insert_pis", NULL}; + static const char * const _keywords[] = {"element_factory", "comment_factory", "pi_factory", "insert_comments", "insert_pis", "insert_cdata", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "TreeBuilder", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[5]; + PyObject *argsbuf[6]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; @@ -1025,6 +1030,7 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar PyObject *pi_factory = Py_None; int insert_comments = 0; int insert_pis = 0; + int insert_cdata = 0; fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -1065,22 +1071,31 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar goto skip_optional_kwonly; } } - insert_pis = PyObject_IsTrue(fastargs[4]); - if (insert_pis < 0) { + if (fastargs[4]) { + insert_pis = PyObject_IsTrue(fastargs[4]); + if (insert_pis < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + insert_cdata = PyObject_IsTrue(fastargs[5]); + if (insert_cdata < 0) { goto exit; } skip_optional_kwonly: - return_value = _elementtree_TreeBuilder___init___impl((TreeBuilderObject *)self, element_factory, comment_factory, pi_factory, insert_comments, insert_pis); + return_value = _elementtree_TreeBuilder___init___impl((TreeBuilderObject *)self, element_factory, comment_factory, pi_factory, insert_comments, insert_pis, insert_cdata); exit: return return_value; } PyDoc_STRVAR(_elementtree__set_factories__doc__, -"_set_factories($module, comment_factory, pi_factory, /)\n" +"_set_factories($module, comment_factory, pi_factory, cdata_type, /)\n" "--\n" "\n" -"Change the factories used to create comments and processing instructions.\n" +"Change the factories used to create comments, PIs and CDATA strings.\n" "\n" "For internal use only."); @@ -1089,7 +1104,7 @@ PyDoc_STRVAR(_elementtree__set_factories__doc__, static PyObject * _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, - PyObject *pi_factory); + PyObject *pi_factory, PyObject *cdata_type); static PyObject * _elementtree__set_factories(PyObject *module, PyObject *const *args, Py_ssize_t nargs) @@ -1097,13 +1112,15 @@ _elementtree__set_factories(PyObject *module, PyObject *const *args, Py_ssize_t PyObject *return_value = NULL; PyObject *comment_factory; PyObject *pi_factory; + PyObject *cdata_type; - if (!_PyArg_CheckPositional("_set_factories", nargs, 2, 2)) { + if (!_PyArg_CheckPositional("_set_factories", nargs, 3, 3)) { goto exit; } comment_factory = args[0]; pi_factory = args[1]; - return_value = _elementtree__set_factories_impl(module, comment_factory, pi_factory); + cdata_type = args[2]; + return_value = _elementtree__set_factories_impl(module, comment_factory, pi_factory, cdata_type); exit: return return_value; @@ -1155,6 +1172,45 @@ _elementtree_TreeBuilder_end(PyObject *self, PyObject *tag) return return_value; } +PyDoc_STRVAR(_elementtree_TreeBuilder_start_cdata__doc__, +"start_cdata($self, /)\n" +"--\n" +"\n" +"Begin a CDATA section.\n" +"\n" +"The data collected until the matching end_cdata() call is the content\n" +"of the section."); + +#define _ELEMENTTREE_TREEBUILDER_START_CDATA_METHODDEF \ + {"start_cdata", (PyCFunction)_elementtree_TreeBuilder_start_cdata, METH_NOARGS, _elementtree_TreeBuilder_start_cdata__doc__}, + +static PyObject * +_elementtree_TreeBuilder_start_cdata_impl(TreeBuilderObject *self); + +static PyObject * +_elementtree_TreeBuilder_start_cdata(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _elementtree_TreeBuilder_start_cdata_impl((TreeBuilderObject *)self); +} + +PyDoc_STRVAR(_elementtree_TreeBuilder_end_cdata__doc__, +"end_cdata($self, /)\n" +"--\n" +"\n" +"End a CDATA section and add its content to the tree."); + +#define _ELEMENTTREE_TREEBUILDER_END_CDATA_METHODDEF \ + {"end_cdata", (PyCFunction)_elementtree_TreeBuilder_end_cdata, METH_NOARGS, _elementtree_TreeBuilder_end_cdata__doc__}, + +static PyObject * +_elementtree_TreeBuilder_end_cdata_impl(TreeBuilderObject *self); + +static PyObject * +_elementtree_TreeBuilder_end_cdata(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return _elementtree_TreeBuilder_end_cdata_impl((TreeBuilderObject *)self); +} + PyDoc_STRVAR(_elementtree_TreeBuilder_comment__doc__, "comment($self, text, /)\n" "--\n" @@ -1479,4 +1535,4 @@ _elementtree_XMLParser__setevents(PyObject *self, PyObject *const *args, Py_ssiz exit: return return_value; } -/*[clinic end generated code: output=e2e9cf288c4400f6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=245871e3334425bb input=a9049054013a1b77]*/ diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 9eb0dddb2488e23..c1ac4e24d9db43b 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -2549,6 +2549,7 @@ pyexpat_exec(PyObject *mod) capi->SetUnknownEncodingHandler = XML_SetUnknownEncodingHandler; capi->SetUserData = XML_SetUserData; capi->SetStartDoctypeDeclHandler = XML_SetStartDoctypeDeclHandler; + capi->SetCdataSectionHandler = XML_SetCdataSectionHandler; capi->SetEncoding = XML_SetEncoding; capi->DefaultUnknownEncodingHandler = PyUnknownEncodingHandler; #if XML_COMBINED_VERSION >= 20100