From 7cb3a24da5dfa4148a9d67bde38c670746b31f81 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 31 Aug 2026 22:04:06 +0300 Subject: [PATCH 1/2] gh-81055: Support CDATA sections in xml.etree.ElementTree CDATA is a new factory, like Comment and ProcessingInstruction, which creates a special element serialized as a CDATA section. Its content is character data: it is not escaped, "]]>" in it is split between two sections, and it is returned by itertext() and by the "text" serialization method. TreeBuilder gets the cdata_factory and insert_cdata arguments. When insert_cdata is set, a CDATA section in the input is kept as such instead of being parsed as text, so that the document can be written back unchanged. Expat reports the content of a CDATA section as ordinary character data, but it reports the boundaries, so the builder gets the start_cdata() and end_cdata() methods, and XMLParser calls them like comment() and pi(). _set_factories() takes the third factory, which the C implementation needs for itertext(), and the pyexpat capsule gets SetCdataSectionHandler. --- Doc/library/xml.etree.elementtree.rst | 61 ++++- Doc/whatsnew/3.16.rst | 16 ++ Include/pyexpat.h | 3 + Lib/test/test_xml_etree.py | 143 +++++++++++- Lib/xml/etree/ElementTree.py | 85 ++++++- ...6-08-31-21-40-00.gh-issue-81055.Wq2nX7.rst | 12 + Modules/_elementtree.c | 211 +++++++++++++++++- Modules/clinic/_elementtree.c.h | 90 ++++++-- Modules/pyexpat.c | 1 + 9 files changed, 586 insertions(+), 36 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-31-21-40-00.gh-issue-81055.Wq2nX7.rst diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 310ccd651e18c7e..a76d73038d29686 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -548,6 +548,28 @@ Functions .. versionadded:: 3.8 +.. function:: CDATA(text=None) + + CDATA section factory. + This factory function creates a special element + which the standard serializer serializes as a CDATA section. + *text* is a string containing the content of the CDATA section. + + The content of a CDATA section is character data: + it is not escaped when serialized, and it is returned + by :meth:`Element.itertext` and by the ``"text"`` serialization method. + ``"]]>"`` cannot occur in a CDATA section, + so the content which contains it is split into several sections. + + Note that a CDATA section in the input is parsed as text by default: + its content is added to the tree as ordinary character data. + A tree only contains CDATA sections if they have been inserted into it + using one of the :class:`Element` methods, + or if the parser target collects them; see :class:`TreeBuilder`. + + .. versionadded:: next + + .. function:: Comment(text=None) Comment element factory. This factory function creates a special element @@ -1051,9 +1073,14 @@ Element Objects Creates a text iterator. The iterator loops over this element and all subelements, in document order, and returns all inner text. + The content of CDATA sections is a part of the inner text, + but the content of comments and processing instructions is not. .. versionadded:: 3.2 + .. versionchanged:: next + The content of CDATA sections is returned. + .. method:: makeelement(tag, attrib) @@ -1270,7 +1297,9 @@ TreeBuilder Objects .. class:: TreeBuilder(element_factory=None, *, comment_factory=None, \ - pi_factory=None, insert_comments=False, insert_pis=False) + pi_factory=None, cdata_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 @@ -1288,6 +1317,16 @@ TreeBuilder Objects comments/pis will be inserted into the tree if they appear within the root element (but not outside of it). + The *cdata_factory* function, when given, + should behave like the :func:`CDATA` function. + If *insert_cdata* is true, a CDATA section in the input is created + with this factory and inserted into the tree; + otherwise its content is added to the tree as ordinary character data + and the factory is not called. + + .. versionchanged:: next + Added the *cdata_factory* and *insert_cdata* arguments. + .. method:: close() Flushes the builder buffers, and returns the toplevel document @@ -1328,6 +1367,26 @@ TreeBuilder Objects .. versionadded:: 3.8 + .. 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, creates a CDATA section + with the collected content and adds it to the tree, + and returns it. Otherwise returns ``None`` + and the collected content is left as ordinary character data. + + .. versionadded:: next + + In addition, a custom :class:`TreeBuilder` object can provide the following methods: diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index f3ddae7a2b2fdd1..310d47a4bedfd6b 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -648,6 +648,22 @@ xml rather than defaulted from the DTD. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* Add :func:`~xml.etree.ElementTree.CDATA` to :mod:`xml.etree.ElementTree`, + a factory of special elements which are serialized as CDATA sections, + like :func:`~xml.etree.ElementTree.Comment` and + :func:`~xml.etree.ElementTree.ProcessingInstruction`. + The content of a CDATA section is character data: + it is not escaped when serialized, and it is returned by + :meth:`~xml.etree.ElementTree.Element.itertext` + and by the ``"text"`` serialization method. + :class:`~xml.etree.ElementTree.TreeBuilder` supports the *cdata_factory* + and *insert_cdata* arguments, which make the parser keep CDATA sections + instead of parsing them as text, and the new :meth:`!start_cdata` and + :meth:`!end_cdata` methods, which :class:`~xml.etree.ElementTree.XMLParser` + calls for the boundaries of a CDATA section, like :meth:`!comment` and + :meth:`!pi`. + (Contributed by Serhiy Storchaka in :gh:`81055`.) + zipfile ------- 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 2af2d1fd64520b1..2cf7a3528452dcc 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -1310,6 +1310,35 @@ def test_comment_serialization(self): # no comments in text serialization self.assertEqual(ET.tostring(comm, method='text'), b'') + def test_cdata_serialization(self): + cdata = ET.CDATA(' & ham') + # the content of a CDATA section is not escaped + self.assertEqual(ET.tostring(cdata), b' & ham]]>') + self.assertEqual(ET.tostring(cdata, method='html'), + b' & ham]]>') + # but it is character data + self.assertEqual(ET.tostring(cdata, method='text'), b' & ham') + # an empty CDATA section + self.assertEqual(ET.tostring(ET.CDATA()), b'') + self.assertEqual(ET.tostring(ET.CDATA('')), b'') + # "]]>" cannot occur in a CDATA section, it is split in two + self.assertEqual(ET.tostring(ET.CDATA('a]]>b')), + b'b]]>') + self.assertRaises(TypeError, ET.tostring, ET.CDATA(42)) + + def test_cdata_in_element(self): + elem = ET.XML('before') + cdata = ET.CDATA(' & ham') + cdata.tail = 'after' + elem.append(cdata) + self.assertEqual(ET.tostring(elem), + b'before & ham]]>after') + self.assertEqual(ET.tostring(elem, method='text'), + b'before & hamafter') + # the written form is parsed back to the same text + self.assertEqual(''.join(ET.fromstring(ET.tostring(elem)).itertext()), + 'before & hamafter') + def test_processinginstruction_serialization(self): # Test ProcessingInstruction directly @@ -3771,6 +3800,21 @@ def test_processinginstruction(self): self.assertEqual(''.join(pi.itertext()), '') self.assertEqual(list(pi.iter()), [pi]) + def test_cdata(self): + e = ET.Element('root') + e.text = 'before' + cdata = ET.CDATA('content') + self.assertEqual(cdata.text, 'content') + cdata.tail = 'after' + e.append(cdata) + # unlike a comment or a processing instruction, + # a CDATA section contains character data + self.assertEqual(''.join(e.itertext()), 'beforecontentafter') + self.assertEqual(list(e.iter()), [e, cdata]) + self.assertEqual(list(e.iter('root')), [e]) + self.assertEqual(''.join(cdata.itertext()), 'content') + self.assertEqual(list(cdata.iter()), [cdata]) + def test_corners(self): # single root, no subelements a = ET.Element('a') @@ -3904,6 +3948,101 @@ 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') + cdata = b.end_cdata() + self.assertEqual(cdata.tag, ET.CDATA) + self.assertEqual(cdata.text, 'a < b') + b.data('after') + b.end('a') + a = b.close() + self.assertEqual(ET.tostring(a), + b'beforeafter') + + def test_treebuilder_cdata_factory(self): + # the factory is only called if insert_cdata is true + b = ET.TreeBuilder(cdata_factory=len) + b.start_cdata() + self.assertIsNone(b.end_cdata()) + + b = ET.TreeBuilder(insert_cdata=True, + cdata_factory=lambda text: ET.Comment('was: ' + text)) + b.start('a', {}) + b.start_cdata() + b.data('abc') + self.assertEqual(b.end_cdata().tag, ET.Comment) + b.end('a') + self.assertEqual(ET.tostring(b.close()), b'') + + 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), [ET.CDATA, 'b']) + self.assertEqual(a.text, 'before') + self.assertEqual(a[0].text, 'a < b') + self.assertEqual(a[0].tail, 'after') + # 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() + self.assertEqual(summarize_list(a), [ET.CDATA]) + self.assertEqual(a[0].text, '') + + 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): @@ -4981,9 +5120,9 @@ def cleanup(): unittest.addModuleCleanup(setattr, ElementPath, "_cache", path_cache) ElementPath._cache = path_cache.copy() - # Align the Comment/PI factories. + # Align the Comment/PI/CDATA factories. 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 951540eb9f45e90..7318ed8a10a37b1 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", @@ -408,7 +409,8 @@ def itertext(self): """ tag = self.tag - if not isinstance(tag, str) and tag is not None: + if not isinstance(tag, str) and tag is not None and tag is not CDATA: + # the content of a CDATA section is character data return t = self.text if t: @@ -471,6 +473,21 @@ def ProcessingInstruction(target, text=None): PI = ProcessingInstruction +def CDATA(text=None): + """CDATA section factory. + + This function creates a special element which the standard serializer + serializes as a CDATA section. The parser never creates such elements: + the content of a CDATA section is character data, and is parsed as text. + + *text* is a string containing the content of the CDATA section. + + """ + element = Element(CDATA) + element.text = text + return element + + class QName: """Qualified name wrapper. @@ -848,7 +865,8 @@ def add_qname(qname): 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: + elif (tag is not None and tag is not Comment and tag is not PI + and tag is not CDATA): _raise_serialization_error(tag) for key, value in elem.items(): if isinstance(key, QName): @@ -870,6 +888,8 @@ def _serialize_xml(write, elem, qnames, namespaces, write("" % text) elif tag is ProcessingInstruction: write("" % text) + elif tag is CDATA: + write(_cdata_section(text)) else: tag = qnames[tag] if tag is None: @@ -926,6 +946,8 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs): write("" % text) elif tag is ProcessingInstruction: write("" % text) + elif tag is CDATA: + write(_cdata_section(text)) else: tag = qnames[tag] if tag is None: @@ -973,8 +995,16 @@ def _serialize_html(write, elem, qnames, namespaces, **kwargs): write(_escape_cdata(elem.tail)) def _serialize_text(write, elem): - for part in elem.itertext(): - write(part) + tag = elem.tag + if tag is CDATA: + # the content of a CDATA section is character data + if elem.text: + write(elem.text) + elif tag is None or isinstance(tag, str): + if elem.text: + write(elem.text) + for e in elem: + _serialize_text(write, e) if elem.tail: write(elem.tail) @@ -1024,6 +1054,18 @@ 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 text is None: + return "" + try: + if "]]>" in text: + # a CDATA section cannot contain "]]>", split it in two + text = text.replace("]]>", "]]]]>") + return "" + except (TypeError, AttributeError): + _raise_serialization_error(text) + def _escape_cdata(text): # escape character data try: @@ -1428,10 +1470,14 @@ 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. + + *cdata_factory* is a factory to create CDATA sections to be used instead + of the standard factory. If *insert_cdata* is false (the default), the + content of CDATA sections is added to the tree as ordinary text. """ def __init__(self, element_factory=None, *, - comment_factory=None, pi_factory=None, - insert_comments=False, insert_pis=False): + comment_factory=None, pi_factory=None, cdata_factory=None, + insert_comments=False, insert_pis=False, insert_cdata=False): self._data = [] # data collector self._elem = [] # element stack self._last = None # last element @@ -1445,6 +1491,10 @@ def __init__(self, element_factory=None, *, pi_factory = ProcessingInstruction self._pi_factory = pi_factory self.insert_pis = insert_pis + if cdata_factory is None: + cdata_factory = CDATA + self._cdata_factory = cdata_factory + self.insert_cdata = insert_cdata if element_factory is None: element_factory = Element self._factory = element_factory @@ -1519,6 +1569,23 @@ 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 text 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 create it using the cdata_factory.""" + if not self.insert_cdata: + return None + text = "".join(self._data) + self._data = [] + return self._handle_single(self._cdata_factory, True, text) + def _handle_single(self, factory, insert, *args): elem = factory(*args) if insert: @@ -1576,6 +1643,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 @@ -2117,7 +2188,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-08-31-21-40-00.gh-issue-81055.Wq2nX7.rst b/Misc/NEWS.d/next/Library/2026-08-31-21-40-00.gh-issue-81055.Wq2nX7.rst new file mode 100644 index 000000000000000..9c8ebdbcb3bc831 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-21-40-00.gh-issue-81055.Wq2nX7.rst @@ -0,0 +1,12 @@ +Add :func:`xml.etree.ElementTree.CDATA`, a factory of special elements +which are serialized as CDATA sections. +Their content is character data: +it is not escaped when serialized, and it is returned by +:meth:`~xml.etree.ElementTree.Element.itertext` +and by the ``"text"`` serialization method. +:class:`~xml.etree.ElementTree.TreeBuilder` now supports +the *cdata_factory* and *insert_cdata* arguments, +which make the parser keep CDATA sections instead of parsing them as text, +and the new :meth:`!start_cdata` and :meth:`!end_cdata` methods, +which :class:`~xml.etree.ElementTree.XMLParser` calls +for the boundaries of a CDATA section. diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index f827274eeffba83..1de1174f3b54217 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_factory; /* 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_factory); // 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_factory); // Heap types Py_VISIT(st->Element_Type); @@ -2298,8 +2301,13 @@ elementiter_next(PyObject *op) } if (it->gettext) { if (elem->tag != Py_None && !PyUnicode_Check(elem->tag)) { - Py_DECREF(elem); - continue; + /* the content of a CDATA section is character data */ + elementtreestate *st = + get_elementtree_state_by_type(Py_TYPE(op)); + if (elem->tag != st->cdata_factory) { + Py_DECREF(elem); + continue; + } } text = element_get_text(elem); goto gettext; @@ -2407,6 +2415,7 @@ typedef struct { PyObject *element_factory; PyObject *comment_factory; PyObject *pi_factory; + PyObject *cdata_factory; /* element tracing */ PyObject *events_append; /* the append method of the list of events, or NULL */ @@ -2419,6 +2428,7 @@ typedef struct { char insert_comments; char insert_pis; + char insert_cdata; elementtreestate *state; } TreeBuilderObject; @@ -2441,6 +2451,7 @@ treebuilder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) t->element_factory = NULL; t->comment_factory = NULL; t->pi_factory = NULL; + t->cdata_factory = NULL; t->stack = PyList_New(20); if (!t->stack) { Py_DECREF(t->this); @@ -2454,7 +2465,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; @@ -2467,8 +2478,10 @@ _elementtree.TreeBuilder.__init__ * comment_factory: object = None pi_factory: object = None + cdata_factory: object = None insert_comments: bool = False insert_pis: bool = False + insert_cdata: bool = False [clinic start generated code]*/ @@ -2477,8 +2490,10 @@ _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=ae98a94df20b5cc3]*/ + PyObject *cdata_factory, + int insert_comments, int insert_pis, + int insert_cdata) +/*[clinic end generated code: output=3ccfba7fd8cdd668 input=64951f52e409494b]*/ { if (element_factory != Py_None) { Py_XSETREF(self->element_factory, Py_NewRef(element_factory)); @@ -2510,6 +2525,18 @@ _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, self->insert_pis = 0; } + if (cdata_factory == Py_None) { + elementtreestate *st = self->state; + cdata_factory = st->cdata_factory; + } + if (cdata_factory) { + Py_XSETREF(self->cdata_factory, Py_NewRef(cdata_factory)); + self->insert_cdata = insert_cdata; + } else { + Py_CLEAR(self->cdata_factory); + self->insert_cdata = 0; + } + return 0; } @@ -2533,6 +2560,7 @@ treebuilder_gc_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(self->stack); Py_VISIT(self->pi_factory); Py_VISIT(self->comment_factory); + Py_VISIT(self->cdata_factory); Py_VISIT(self->element_factory); return 0; } @@ -2553,6 +2581,7 @@ treebuilder_gc_clear(PyObject *op) Py_CLEAR(self->last); Py_CLEAR(self->last_for_tail); Py_CLEAR(self->this); + Py_CLEAR(self->cdata_factory); Py_CLEAR(self->pi_factory); Py_CLEAR(self->comment_factory); Py_CLEAR(self->element_factory); @@ -2574,22 +2603,25 @@ treebuilder_dealloc(PyObject *self) /* helpers for handling of arbitrary element-like objects */ /*[clinic input] -@permit_long_summary _elementtree._set_factories comment_factory: object pi_factory: object + cdata_factory: object / -Change the factories used to create comments and processing instructions. +Change the factories used to create special elements. + +They create comments, processing instructions and CDATA sections. 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_factory) +/*[clinic end generated code: output=bb15ac96eeda66af input=f61d1ce3068ef3f2]*/ { elementtreestate *st = get_elementtree_state(module); PyObject *old; @@ -2604,10 +2636,20 @@ _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, Py_TYPE(pi_factory)->tp_name); return NULL; } + if (!PyCallable_Check(cdata_factory) && cdata_factory != Py_None) { + PyErr_Format(PyExc_TypeError, + "CDATA factory must be callable, not %.100s", + Py_TYPE(cdata_factory)->tp_name); + return NULL; + } - old = _PyTuple_FromPair( + 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_factory ? st->cdata_factory : Py_None); + if (old == NULL) { + return NULL; + } if (comment_factory == Py_None) { Py_CLEAR(st->comment_factory); @@ -2619,6 +2661,11 @@ _elementtree__set_factories_impl(PyObject *module, PyObject *comment_factory, } else { Py_XSETREF(st->pi_factory, Py_NewRef(pi_factory)); } + if (cdata_factory == Py_None) { + Py_CLEAR(st->cdata_factory); + } else { + Py_XSETREF(st->cdata_factory, Py_NewRef(cdata_factory)); + } return old; } @@ -2919,6 +2966,56 @@ treebuilder_handle_comment(TreeBuilderObject* self, PyObject* text) return NULL; } +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) +{ + PyObject* text; + PyObject* cdata; + PyObject* this; + + if (!self->insert_cdata) { + 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(self->cdata_factory, text); + Py_DECREF(text); + if (!cdata) { + return NULL; + } + + this = self->this; + if (this != Py_None) { + if (treebuilder_add_subelement(self->state, this, cdata) < 0) { + Py_DECREF(cdata); + return NULL; + } + Py_XSETREF(self->last_for_tail, Py_NewRef(cdata)); + } + + return cdata; +} + LOCAL(PyObject*) treebuilder_handle_pi(TreeBuilderObject* self, PyObject* target, PyObject* text) { @@ -3028,6 +3125,37 @@ _elementtree_TreeBuilder_end_impl(TreeBuilderObject *self, PyObject *tag) return treebuilder_handle_end(self, tag); } +/*[clinic input] +_elementtree.TreeBuilder.start_cdata + +Begin a CDATA section. + +[clinic start generated code]*/ + +static PyObject * +_elementtree_TreeBuilder_start_cdata_impl(TreeBuilderObject *self) +/*[clinic end generated code: output=433004da9adec83c input=43afb6bdadd7d183]*/ +{ + if (treebuilder_handle_cdata_start(self) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + +/*[clinic input] +_elementtree.TreeBuilder.end_cdata + +End a CDATA section and create it using the cdata_factory. + +[clinic start generated code]*/ + +static PyObject * +_elementtree_TreeBuilder_end_cdata_impl(TreeBuilderObject *self) +/*[clinic end generated code: output=7795f7e05cd36223 input=1d292f199287c164]*/ +{ + return treebuilder_handle_cdata_end(self); +} + /*[clinic input] _elementtree.TreeBuilder.comment @@ -3130,6 +3258,8 @@ typedef struct { PyObject *handle_end; PyObject *handle_comment; + PyObject *handle_start_cdata; + PyObject *handle_end_cdata; PyObject *handle_pi; PyObject *handle_doctype; @@ -3532,6 +3662,44 @@ expat_end_ns_handler(void *op, const XML_Char *prefix_in) Py_XDECREF(res); } +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_start_cdata) { + PyObject *res = PyObject_CallNoArgs(self->handle_start_cdata); + Py_XDECREF(res); + } +} + +static void +expat_end_cdata_handler(void *op) +{ + XMLParserObject *self = XMLParserObject_CAST(op); + + if (PyErr_Occurred()) + return; + + elementtreestate *st = self->state; + if (TreeBuilder_CheckExact(st, self->target)) { + /* shortcut */ + PyObject *res = + treebuilder_handle_cdata_end((TreeBuilderObject*) self->target); + Py_XDECREF(res); + } else if (self->handle_end_cdata) { + PyObject *res = PyObject_CallNoArgs(self->handle_end_cdata); + Py_XDECREF(res); + } +} + static void expat_comment_handler(void *op, const XML_Char *comment_in) { @@ -3687,6 +3855,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_start_cdata = self->handle_end_cdata = NULL; self->handle_doctype = NULL; self->elementtree_module = PyType_GetModuleByDef(type, &elementtreemodule); assert(self->elementtree_module != NULL); @@ -3786,6 +3955,14 @@ _elementtree_XMLParser___init___impl(XMLParserObject *self, PyObject *target, if (ignore_attribute_error(self->handle_comment)) { return -1; } + self->handle_start_cdata = PyObject_GetAttrString(target, "start_cdata"); + if (ignore_attribute_error(self->handle_start_cdata)) { + return -1; + } + self->handle_end_cdata = PyObject_GetAttrString(target, "end_cdata"); + if (ignore_attribute_error(self->handle_end_cdata)) { + return -1; + } self->handle_pi = PyObject_GetAttrString(target, "pi"); if (ignore_attribute_error(self->handle_pi)) { return -1; @@ -3830,6 +4007,12 @@ _elementtree_XMLParser___init___impl(XMLParserObject *self, PyObject *target, self->parser, (XML_ProcessingInstructionHandler) expat_pi_handler ); + if (self->handle_start_cdata || self->handle_end_cdata) + 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 @@ -3850,6 +4033,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_start_cdata); + Py_VISIT(self->handle_end_cdata); Py_VISIT(self->handle_end); Py_VISIT(self->handle_data); Py_VISIT(self->handle_start); @@ -3879,6 +4064,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_start_cdata); + Py_CLEAR(self->handle_end_cdata); Py_CLEAR(self->handle_end); Py_CLEAR(self->handle_data); Py_CLEAR(self->handle_start); @@ -4377,6 +4564,8 @@ static PyMethodDef treebuilder_methods[] = { _ELEMENTTREE_TREEBUILDER_START_METHODDEF _ELEMENTTREE_TREEBUILDER_END_METHODDEF _ELEMENTTREE_TREEBUILDER_COMMENT_METHODDEF + _ELEMENTTREE_TREEBUILDER_START_CDATA_METHODDEF + _ELEMENTTREE_TREEBUILDER_END_CDATA_METHODDEF _ELEMENTTREE_TREEBUILDER_PI_METHODDEF _ELEMENTTREE_TREEBUILDER_CLOSE_METHODDEF {NULL, NULL} diff --git a/Modules/clinic/_elementtree.c.h b/Modules/clinic/_elementtree.c.h index c9e77a4c2b92d8b..d6851a5dda05f46 100644 --- a/Modules/clinic/_elementtree.c.h +++ b/Modules/clinic/_elementtree.c.h @@ -864,7 +864,9 @@ _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, PyObject *element_factory, PyObject *comment_factory, PyObject *pi_factory, - int insert_comments, int insert_pis); + PyObject *cdata_factory, + int insert_comments, int insert_pis, + int insert_cdata); static int _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwargs) @@ -872,7 +874,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 7 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -881,7 +883,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(cdata_factory), &_Py_ID(insert_comments), &_Py_ID(insert_pis), &_Py_ID(insert_cdata), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -890,22 +892,24 @@ _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", "cdata_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[7]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; PyObject *element_factory = Py_None; PyObject *comment_factory = Py_None; PyObject *pi_factory = Py_None; + PyObject *cdata_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); @@ -938,7 +942,13 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar } } if (fastargs[3]) { - insert_comments = PyObject_IsTrue(fastargs[3]); + cdata_factory = fastargs[3]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (fastargs[4]) { + insert_comments = PyObject_IsTrue(fastargs[4]); if (insert_comments < 0) { goto exit; } @@ -946,22 +956,33 @@ _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[5]) { + insert_pis = PyObject_IsTrue(fastargs[5]); + if (insert_pis < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + insert_cdata = PyObject_IsTrue(fastargs[6]); + 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, cdata_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_factory, /)\n" "--\n" "\n" -"Change the factories used to create comments and processing instructions.\n" +"Change the factories used to create special elements.\n" +"\n" +"They create comments, processing instructions and CDATA sections.\n" "\n" "For internal use only."); @@ -970,7 +991,8 @@ 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_factory); static PyObject * _elementtree__set_factories(PyObject *module, PyObject *const *args, Py_ssize_t nargs) @@ -978,13 +1000,15 @@ _elementtree__set_factories(PyObject *module, PyObject *const *args, Py_ssize_t PyObject *return_value = NULL; PyObject *comment_factory; PyObject *pi_factory; + PyObject *cdata_factory; - 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_factory = args[2]; + return_value = _elementtree__set_factories_impl(module, comment_factory, pi_factory, cdata_factory); exit: return return_value; @@ -1032,6 +1056,42 @@ _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."); + +#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 create it using the cdata_factory."); + +#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" @@ -1331,4 +1391,4 @@ _elementtree_XMLParser__setevents(PyObject *self, PyObject *const *args, Py_ssiz exit: return return_value; } -/*[clinic end generated code: output=c863ce16d8566291 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b9470f3ef3e8f67a input=a9049054013a1b77]*/ diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index fa8b0db60806233..d154ccdd570a291 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -2542,6 +2542,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 From 19d06f70ff49d234c75b7580fbeb935ee144268f Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 31 Aug 2026 23:44:01 +0300 Subject: [PATCH 2/2] Regenerate global objects for the new keyword arguments --- Include/internal/pycore_global_objects_fini_generated.h | 2 ++ Include/internal/pycore_global_strings.h | 2 ++ Include/internal/pycore_runtime_init_generated.h | 2 ++ Include/internal/pycore_unicodeobject_generated.h | 8 ++++++++ 4 files changed, 14 insertions(+) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 9ab20be70614def..d35d5bb5f857fe3 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1645,6 +1645,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(capitals)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(category)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cb_type)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(cdata_factory)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(certfile)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(chain)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(check_same_thread)); @@ -1842,6 +1843,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 51d9fbe89b34235..5211d5f22acd9ad 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -368,6 +368,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(capitals) STRUCT_FOR_ID(category) STRUCT_FOR_ID(cb_type) + STRUCT_FOR_ID(cdata_factory) STRUCT_FOR_ID(certfile) STRUCT_FOR_ID(chain) STRUCT_FOR_ID(check_same_thread) @@ -565,6 +566,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 88ca09e6ba245f0..31399222b55d9a3 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1643,6 +1643,7 @@ extern "C" { INIT_ID(capitals), \ INIT_ID(category), \ INIT_ID(cb_type), \ + INIT_ID(cdata_factory), \ INIT_ID(certfile), \ INIT_ID(chain), \ INIT_ID(check_same_thread), \ @@ -1840,6 +1841,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 3c4d7d664537a8a..0d267be80221286 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1252,6 +1252,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(cdata_factory); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(certfile); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2040,6 +2044,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));