From e1c047039b72c4c9002e9eb9c6cf689f4f54a9b2 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 28 Sep 2025 23:46:55 +0200 Subject: [PATCH 01/22] pyexpat.c: Disallow collection of in-use parent parsers Within libexpat, a parser created via XML_ExternalEntityParserCreate is relying on its parent parser throughout its entire lifetime. Prior to this fix, is was possible for the parent parser to be garbage-collected by CPython. This fixes related reference counting, to stop that from happening. --- Modules/pyexpat.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 7f6d84ad8641ca..f870fed0d69550 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -72,10 +72,11 @@ pyexpat_get_state(PyObject *module) /* Declarations for objects of type xmlparser */ -typedef struct { +typedef struct xmlparseobject_ { PyObject_HEAD XML_Parser itself; + struct xmlparseobject_ * parent; int ordered_attributes; /* Return attributes as a list. */ int specified_attributes; /* Report only specified attributes. */ int in_callback; /* Is a callback active? */ @@ -1065,6 +1066,11 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self, return NULL; } + // The new subparser will make use of the parent XML_Parser inside of Expat. + // So we need to take subparsers into account with the reference counting + // of their parent parser. + Py_INCREF(self); + new_parser->buffer_size = self->buffer_size; new_parser->buffer_used = 0; new_parser->buffer = NULL; @@ -1074,6 +1080,7 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self, new_parser->ns_prefixes = self->ns_prefixes; new_parser->itself = XML_ExternalEntityParserCreate(self->itself, context, encoding); + new_parser->parent = self; new_parser->handlers = 0; new_parser->intern = Py_XNewRef(self->intern); @@ -1081,11 +1088,13 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self, new_parser->buffer = PyMem_Malloc(new_parser->buffer_size); if (new_parser->buffer == NULL) { Py_DECREF(new_parser); + Py_DECREF(self); return PyErr_NoMemory(); } } if (!new_parser->itself) { Py_DECREF(new_parser); + Py_DECREF(self); return PyErr_NoMemory(); } @@ -1099,6 +1108,7 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self, new_parser->handlers = PyMem_New(PyObject *, i); if (!new_parser->handlers) { Py_DECREF(new_parser); + Py_DECREF(self); return PyErr_NoMemory(); } clear_handlers(new_parser, 1); @@ -1479,6 +1489,7 @@ newxmlparseobject(pyexpat_state *state, const char *encoding, /* namespace_separator is either NULL or contains one char + \0 */ self->itself = XML_ParserCreate_MM(encoding, &ExpatMemoryHandler, namespace_separator); + self->parent = NULL; if (self->itself == NULL) { PyErr_SetString(PyExc_RuntimeError, "XML_ParserCreate failed"); @@ -1538,6 +1549,10 @@ xmlparse_dealloc(PyObject *op) XML_ParserFree(self->itself); } self->itself = NULL; + if (self->parent != NULL) { + Py_DECREF(self->parent); + self->parent = NULL; + } if (self->handlers != NULL) { PyMem_Free(self->handlers); From 3b0afdaaf7b2361e209d725c9901f5f29c0d64b5 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 28 Sep 2025 23:56:09 +0200 Subject: [PATCH 02/22] Add a regression test for issue 139400 --- Lib/test/test_pyexpat.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 8e0f7374b26fd0..00f03ffa5e806d 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -771,6 +771,21 @@ def resolve_entity(context, base, system_id, public_id): self.assertEqual(handler_call_args, [("bar", "baz")]) +class ParentParserLifetimeTest(unittest.TestCase): + """ + Subparser make use of the parent XML_Parser inside of Expat. + As a result, parent parsers need to outlive subparsers. + Regression test for issue 139400 + """ + def test_parent_parser_outlives_its_subparsers(self): + parser = expat.ParserCreate() + subparser = parser.ExternalEntityParserCreate(None) + + # Now try to cause garbage collection of the parent parser + # while it's still being referenced by a related subparser + del parser + + class ReparseDeferralTest(unittest.TestCase): def test_getter_setter_round_trip(self): parser = expat.ParserCreate() From 8f70991fadc1dfaa7f9c800c9a760dea4107afaf Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 00:01:30 +0200 Subject: [PATCH 03/22] Add news item for issue 139400 --- .../2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst new file mode 100644 index 00000000000000..c1874910184575 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst @@ -0,0 +1,3 @@ +Make sure that parent Expat parsers are only garbage-collected once they are +no longer being referenced by subparsers. +(Bugfix contributed by Sebastian Pipping) From a63bd34ac2cbeadaece3ade4bba648455ca4f44a Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 00:50:13 +0200 Subject: [PATCH 04/22] Improve test's docstring --- Lib/test/test_pyexpat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 00f03ffa5e806d..03417bea9ccedb 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -773,7 +773,7 @@ def resolve_entity(context, base, system_id, public_id): class ParentParserLifetimeTest(unittest.TestCase): """ - Subparser make use of the parent XML_Parser inside of Expat. + Subparsers make use of their parent XML_Parser inside of Expat. As a result, parent parsers need to outlive subparsers. Regression test for issue 139400 """ From 22417ec475d8e9a0f29caedc23d8584132ebf768 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 01:04:21 +0200 Subject: [PATCH 05/22] Simplify xmlparseobject_ to xmlparseobject Idea by @picnixz --- Modules/pyexpat.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index f870fed0d69550..3047b2acd9045d 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -72,11 +72,11 @@ pyexpat_get_state(PyObject *module) /* Declarations for objects of type xmlparser */ -typedef struct xmlparseobject_ { +typedef struct xmlparseobject { PyObject_HEAD XML_Parser itself; - struct xmlparseobject_ * parent; + struct xmlparseobject * parent; int ordered_attributes; /* Return attributes as a list. */ int specified_attributes; /* Report only specified attributes. */ int in_callback; /* Is a callback active? */ From 20caacd3e77ef29780e5aa1d054ae5a3476f10a4 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 01:07:16 +0200 Subject: [PATCH 06/22] Leverage Py_CLEAR Idea by @picnixz --- Modules/pyexpat.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 3047b2acd9045d..ec3463abe7147e 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1550,8 +1550,7 @@ xmlparse_dealloc(PyObject *op) } self->itself = NULL; if (self->parent != NULL) { - Py_DECREF(self->parent); - self->parent = NULL; + Py_CLEAR(self->parent); } if (self->handlers != NULL) { From f8c58a3040b2deb9b8d37196a4697c9ebf04abd6 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 01:45:54 +0200 Subject: [PATCH 07/22] Simplify call to Py_CLEAR Idea by @picnixz --- Modules/pyexpat.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index ec3463abe7147e..2f23696cd7b53d 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1549,9 +1549,7 @@ xmlparse_dealloc(PyObject *op) XML_ParserFree(self->itself); } self->itself = NULL; - if (self->parent != NULL) { - Py_CLEAR(self->parent); - } + Py_CLEAR(self->parent); if (self->handlers != NULL) { PyMem_Free(self->handlers); From f1e3e3624b1e443246a36cd0d20b09652e62edd6 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 01:51:24 +0200 Subject: [PATCH 08/22] Streamline docsting of ParentParserLifetimeTest Idea by @picnixz --- Lib/test/test_pyexpat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 03417bea9ccedb..83d25825045288 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -775,7 +775,8 @@ class ParentParserLifetimeTest(unittest.TestCase): """ Subparsers make use of their parent XML_Parser inside of Expat. As a result, parent parsers need to outlive subparsers. - Regression test for issue 139400 + + See https://github.com/python/cpython/issues/139400. """ def test_parent_parser_outlives_its_subparsers(self): parser = expat.ParserCreate() From 1b2937d5ee019220ea05bf444bbea7c2ff7732ef Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 01:52:36 +0200 Subject: [PATCH 09/22] pyexpat.c: Adjust whitespace around pointer As requested. --- Modules/pyexpat.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 2f23696cd7b53d..b834e48ba4e13a 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -76,7 +76,7 @@ typedef struct xmlparseobject { PyObject_HEAD XML_Parser itself; - struct xmlparseobject * parent; + struct xmlparseobject *parent; int ordered_attributes; /* Return attributes as a list. */ int specified_attributes; /* Report only specified attributes. */ int in_callback; /* Is a callback active? */ From 8c5a4e868ba370d744647826581858e3f3416a9d Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 29 Sep 2025 22:17:26 +0200 Subject: [PATCH 10/22] Re-write news item As suggested by @picnixz --- .../2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst index c1874910184575..a5dea3b5f8147a 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-29-00-01-28.gh-issue-139400.X2T-jO.rst @@ -1,3 +1,4 @@ -Make sure that parent Expat parsers are only garbage-collected once they are -no longer being referenced by subparsers. -(Bugfix contributed by Sebastian Pipping) +:mod:`xml.parsers.expat`: Make sure that parent Expat parsers are only +garbage-collected once they are no longer referenced by subparsers created +by :meth:`~xml.parsers.expat.xmlparser.ExternalEntityParserCreate`. +Patch by Sebastian Pipping. From a6f5520c3efb55f00e189c030415f379cdabad95 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Tue, 30 Sep 2025 15:14:19 +0200 Subject: [PATCH 11/22] Turn parent into a PyObject pointer Suggested by @picnixz --- Modules/pyexpat.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index b834e48ba4e13a..7d43a750f908e3 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -72,11 +72,11 @@ pyexpat_get_state(PyObject *module) /* Declarations for objects of type xmlparser */ -typedef struct xmlparseobject { +typedef struct { PyObject_HEAD XML_Parser itself; - struct xmlparseobject *parent; + PyObject *parent; /* Parent xmlparseobject (for ref counting) */ int ordered_attributes; /* Return attributes as a list. */ int specified_attributes; /* Report only specified attributes. */ int in_callback; /* Is a callback active? */ @@ -1080,7 +1080,7 @@ pyexpat_xmlparser_ExternalEntityParserCreate_impl(xmlparseobject *self, new_parser->ns_prefixes = self->ns_prefixes; new_parser->itself = XML_ExternalEntityParserCreate(self->itself, context, encoding); - new_parser->parent = self; + new_parser->parent = (PyObject *)self; new_parser->handlers = 0; new_parser->intern = Py_XNewRef(self->intern); From 4470ceabf12be222c2d2a407f32841e591ff52f3 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Tue, 30 Sep 2025 15:49:48 +0200 Subject: [PATCH 12/22] Add missing parent visit to xmlparse_traverse Kudos to @picnixz --- Modules/pyexpat.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 7d43a750f908e3..01ac6e32ba74d3 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1526,6 +1526,7 @@ xmlparse_traverse(PyObject *op, visitproc visit, void *arg) for (size_t i = 0; handler_info[i].name != NULL; i++) { Py_VISIT(self->handlers[i]); } + Py_VISIT(self->parent); Py_VISIT(Py_TYPE(op)); return 0; } From 3759ac792315617e04f5f19a56bb6c9ad9d9c856 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Tue, 30 Sep 2025 16:03:20 +0200 Subject: [PATCH 13/22] Re-write comment about mission of "PyObject *parent" As suggested by @picnixz --- Modules/pyexpat.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 01ac6e32ba74d3..34701ce0670c7c 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -76,7 +76,15 @@ typedef struct { PyObject_HEAD XML_Parser itself; - PyObject *parent; /* Parent xmlparseobject (for ref counting) */ + /* + * Strong reference to a parent `xmlparseobject` if this parser + * is a child parser. Set to NULL if this parser is a root parser. + * This is needed to keep the parent parser alive as long as it has + * at least one child parser. + * + * See https://github.com/python/cpython/issues/139400 for details. + */ + PyObject *parent; int ordered_attributes; /* Return attributes as a list. */ int specified_attributes; /* Report only specified attributes. */ int in_callback; /* Is a callback active? */ From 30598a95d4bec1b0c767dced19a13e4b7c3e4f30 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Thu, 2 Oct 2025 17:36:34 +0200 Subject: [PATCH 14/22] Add whitespace as requested --- Lib/test/test_pyexpat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 83d25825045288..b7e8d7feef2b7c 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -778,6 +778,7 @@ class ParentParserLifetimeTest(unittest.TestCase): See https://github.com/python/cpython/issues/139400. """ + def test_parent_parser_outlives_its_subparsers(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) From 6d3c424ffddc6373debb555052234b3b3d4717f6 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Thu, 2 Oct 2025 17:48:17 +0200 Subject: [PATCH 15/22] Add more tests Based on ideas by @picnixz --- Lib/test/test_pyexpat.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index b7e8d7feef2b7c..c0d522155f27d2 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -779,7 +779,7 @@ class ParentParserLifetimeTest(unittest.TestCase): See https://github.com/python/cpython/issues/139400. """ - def test_parent_parser_outlives_its_subparsers(self): + def test_parent_parser_outlives_its_subparsers__single(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) @@ -787,6 +787,31 @@ def test_parent_parser_outlives_its_subparsers(self): # while it's still being referenced by a related subparser del parser + def test_parent_parser_outlives_its_subparsers__multiple(self): + parser = expat.ParserCreate() + subparser_one = parser.ExternalEntityParserCreate(None) + subparser_two = parser.ExternalEntityParserCreate(None) + + # Now try to cause garbage collection of the parent parser + # while it's still being referenced by a related subparser + del parser + + def test_parent_parser_outlives_its_subparsers__chain(self): + parser = expat.ParserCreate() + subparser = parser.ExternalEntityParserCreate(None) + subsubparser = subparser.ExternalEntityParserCreate(None) + + # Now try to cause garbage collection of the parent parsers + # while they are still being referenced by a related subparser + del parser + del subparser + + def test_cycle(self): + parser = expat.ParserCreate() + subparser = parser.ExternalEntityParserCreate(None) + parser.StartElementHandler = lambda _1, _2: subparser + parser.Parse('', True) + class ReparseDeferralTest(unittest.TestCase): def test_getter_setter_round_trip(self): From 66b77e0c2d7cf1d1199d9c678001830858787fe2 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 13:26:41 +0200 Subject: [PATCH 16/22] Add comment about Py_CLEAR(self->parent) to xmlparse_clear --- Modules/pyexpat.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 34701ce0670c7c..1362d16d92402a 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1545,6 +1545,11 @@ xmlparse_clear(PyObject *op) xmlparseobject *self = xmlparseobject_CAST(op); clear_handlers(self, 0); Py_CLEAR(self->intern); + // NOTE: We cannot call Py_CLEAR(self->parent) prior to calling + // XML_ParserFree(self->itself) or a subparser (created via + // XML_ExternalEntityParserCreate could lose its parent XML_Parser + // while still making use of it internally. + // https://github.com/python/cpython/issues/139400 return 0; } From 455787fe8ececc089adf34a17372b4dbad23659b Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 13:29:50 +0200 Subject: [PATCH 17/22] Add missing full stops to docstrings --- Lib/test/test_pyexpat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index c0d522155f27d2..8dbc60dacf6da2 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -784,7 +784,7 @@ def test_parent_parser_outlives_its_subparsers__single(self): subparser = parser.ExternalEntityParserCreate(None) # Now try to cause garbage collection of the parent parser - # while it's still being referenced by a related subparser + # while it's still being referenced by a related subparser. del parser def test_parent_parser_outlives_its_subparsers__multiple(self): @@ -793,7 +793,7 @@ def test_parent_parser_outlives_its_subparsers__multiple(self): subparser_two = parser.ExternalEntityParserCreate(None) # Now try to cause garbage collection of the parent parser - # while it's still being referenced by a related subparser + # while it's still being referenced by a related subparser. del parser def test_parent_parser_outlives_its_subparsers__chain(self): @@ -802,7 +802,7 @@ def test_parent_parser_outlives_its_subparsers__chain(self): subsubparser = subparser.ExternalEntityParserCreate(None) # Now try to cause garbage collection of the parent parsers - # while they are still being referenced by a related subparser + # while they are still being referenced by a related subparser. del parser del subparser From a66945436a8fba28919baadfa2d09ffa4679ef84 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 13:40:07 +0200 Subject: [PATCH 18/22] Drop reference to XML_ExternalEntityParserCreate .. as requested by @picnixz; also add a comma. --- Modules/pyexpat.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 1362d16d92402a..7e73850e671d59 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -1546,9 +1546,8 @@ xmlparse_clear(PyObject *op) clear_handlers(self, 0); Py_CLEAR(self->intern); // NOTE: We cannot call Py_CLEAR(self->parent) prior to calling - // XML_ParserFree(self->itself) or a subparser (created via - // XML_ExternalEntityParserCreate could lose its parent XML_Parser - // while still making use of it internally. + // XML_ParserFree(self->itself), or a subparser could lose its parent + // XML_Parser while still making use of it internally. // https://github.com/python/cpython/issues/139400 return 0; } From a91ed6ba476333a307273fa1f3b27e6ecd6a3ae7 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 14:26:27 +0200 Subject: [PATCH 19/22] Make test test_cycle waterproof --- Lib/test/test_pyexpat.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 8dbc60dacf6da2..1b34f2e4cab2ac 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -3,6 +3,7 @@ import abc import functools +import gc import os import re import sys @@ -809,8 +810,18 @@ def test_parent_parser_outlives_its_subparsers__chain(self): def test_cycle(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) - parser.StartElementHandler = lambda _1, _2: subparser - parser.Parse('', True) + + # Hack a cycle onto it; note that parsing now would not work. + parser.CharacterDataHandler = subparser + + # Self-test that the cycle is real + self.assertIn(parser, gc.get_referents(subparser)) + self.assertIn(subparser, gc.get_referents(parser)) + + # Now try to cause garbage collection of the parent parsers + # while they are still being referenced by a related subparser. + del parser + del subparser class ReparseDeferralTest(unittest.TestCase): From c056cfe0f420bc62fd6a6e4ddfac3715dc2cd6b4 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 14:30:17 +0200 Subject: [PATCH 20/22] Full stops and sentences --- Lib/test/test_pyexpat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 1b34f2e4cab2ac..15dee994e2b165 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -811,10 +811,10 @@ def test_cycle(self): parser = expat.ParserCreate() subparser = parser.ExternalEntityParserCreate(None) - # Hack a cycle onto it; note that parsing now would not work. + # This hacks a cycle onto it; note that parsing now would not work. parser.CharacterDataHandler = subparser - # Self-test that the cycle is real + # This self-tests that the cycle is real. self.assertIn(parser, gc.get_referents(subparser)) self.assertIn(subparser, gc.get_referents(parser)) From 12b89892f1f53690c412e9a39f827f04bfaf6602 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 14:48:54 +0200 Subject: [PATCH 21/22] Drop test test_cycle --- Lib/test/test_pyexpat.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 15dee994e2b165..5ad4908be0c23b 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -807,22 +807,6 @@ def test_parent_parser_outlives_its_subparsers__chain(self): del parser del subparser - def test_cycle(self): - parser = expat.ParserCreate() - subparser = parser.ExternalEntityParserCreate(None) - - # This hacks a cycle onto it; note that parsing now would not work. - parser.CharacterDataHandler = subparser - - # This self-tests that the cycle is real. - self.assertIn(parser, gc.get_referents(subparser)) - self.assertIn(subparser, gc.get_referents(parser)) - - # Now try to cause garbage collection of the parent parsers - # while they are still being referenced by a related subparser. - del parser - del subparser - class ReparseDeferralTest(unittest.TestCase): def test_getter_setter_round_trip(self): From 0541d6f48c209dfaed8783a4171a68f19a0aa4bf Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Sun, 5 Oct 2025 15:06:36 +0200 Subject: [PATCH 22/22] Drop now-unused import --- Lib/test/test_pyexpat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 5ad4908be0c23b..b4ce72dfd51774 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -3,7 +3,6 @@ import abc import functools -import gc import os import re import sys