Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions Lib/test/test_free_threading/test_xml_etree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import unittest

from test.support import import_helper, threading_helper
from test.support.threading_helper import run_concurrently

# The C accelerator keeps text delivered by expat in several chunks as a
# list and joins it on first access.
ET = import_helper.import_fresh_module('xml.etree.ElementTree',
fresh=['_elementtree'])

NTHREADS = 8
NITEMS = 500

# Each item text arrives as three chunks ('name ', '&', ' N') and each
# tail as two ('\n', ' ').
DOC = ("<root>\n" +
"".join(f" <item>name &amp; {i}</item>\n" for i in range(NITEMS)) +
" </root>\n")


@threading_helper.requires_working_threading()
@unittest.skipIf(ET is None, 'requires _elementtree')
class TestElementConcurrentReads(unittest.TestCase):
def check(self, worker, nrounds=20):
for _ in range(nrounds):
root = ET.fromstring(DOC)
run_concurrently(worker_func=worker, nthreads=NTHREADS,
args=(root,))

def test_concurrent_text_tail(self):
def worker(root):
for i, item in enumerate(root):
self.assertEqual(item.text, f"name & {i}")
self.assertEqual(item.tail, "\n ")

self.check(worker)

def test_concurrent_tostring(self):
expected = ET.tostring(ET.fromstring(DOC))

def worker(root):
self.assertEqual(ET.tostring(root), expected)

self.check(worker)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a crash in the :term:`free-threaded build` when multiple threads
concurrently read the :attr:`~xml.etree.ElementTree.Element.text` or
:attr:`~xml.etree.ElementTree.Element.tail` attribute of a freshly parsed
:class:`xml.etree.ElementTree.Element` for the first time.
65 changes: 38 additions & 27 deletions Modules/_elementtree.c
Original file line number Diff line number Diff line change
Expand Up @@ -571,45 +571,56 @@ element_get_attrib(ElementObject* self)
}

LOCAL(PyObject*)
element_get_text(ElementObject* self)
element_get_joined(ElementObject* self, PyObject **slot)
{
/* return new reference to text attribute */
/* return new reference to the text or tail attribute stored in *slot,
joining a list of chunks left there by the tree builder on first
access. */

PyObject *res = self->text;
PyObject *res;

if (JOIN_GET(res)) {
res = JOIN_OBJ(res);
if (PyList_CheckExact(res)) {
PyObject *tmp = list_join(res);
if (!tmp)
return NULL;
self->text = tmp;
Py_SETREF(res, tmp);
Py_BEGIN_CRITICAL_SECTION(self);
res = *slot;
if (JOIN_GET(res) && PyList_CheckExact(JOIN_OBJ(res))) {
/* The critical section may be suspended while PyUnicode_Join()
locks the list, letting another thread join and release it
first: hold a reference and re-check *slot before storing. */
PyObject *list = Py_NewRef(JOIN_OBJ(res));
PyObject *joined = list_join(list);
if (joined == NULL) {
res = NULL;
}
else if (*slot == res) {
_set_joined_ptr(slot, joined);
res = joined;
}
else {
Py_DECREF(joined);
res = JOIN_OBJ(*slot);
}
Py_DECREF(list);
}
else {
res = JOIN_OBJ(res);
}
Py_XINCREF(res);
Py_END_CRITICAL_SECTION();

return Py_NewRef(res);
return res;
}

LOCAL(PyObject*)
element_get_text(ElementObject* self)
{
/* return new reference to text attribute */
return element_get_joined(self, &self->text);
}

LOCAL(PyObject*)
element_get_tail(ElementObject* self)
{
/* return new reference to tail attribute */

PyObject *res = self->tail;

if (JOIN_GET(res)) {
res = JOIN_OBJ(res);
if (PyList_CheckExact(res)) {
PyObject *tmp = list_join(res);
if (!tmp)
return NULL;
self->tail = tmp;
Py_SETREF(res, tmp);
}
}

return Py_NewRef(res);
return element_get_joined(self, &self->tail);
}

/*[clinic input]
Expand Down
Loading