From b0a8a64faf900edc69c37dc5238bb025c9ff6473 Mon Sep 17 00:00:00 2001 From: Kira K Date: Tue, 17 Jun 2025 13:42:18 -0400 Subject: [PATCH 01/18] Tenuous first fix Need to switch machines to test on Linux --- Lib/xml/etree/ElementTree.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 44ab5d18624e73..64639bc61cdb5b 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -528,6 +528,9 @@ class ElementTree: """ def __init__(self, element=None, file=None): # assert element is None or iselement(element) + if element is not None and not iselement(element): + raise TypeError(f"element must be etree.Element, " + f"not {type(element).__name__}") self._root = element # first node if file: self.parse(file) @@ -544,6 +547,8 @@ def _setroot(self, element): """ # assert iselement(element) + if not iselement(element): + raise TypeError self._root = element def parse(self, source, parser=None): From f0356c8c8ff58bc414f65ebb803029e0a983cc65 Mon Sep 17 00:00:00 2001 From: Kira Kaviani Date: Tue, 17 Jun 2025 14:48:11 -0400 Subject: [PATCH 02/18] Remove commented assertions --- Lib/xml/etree/ElementTree.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 64639bc61cdb5b..9a2d638fa15c37 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -527,7 +527,6 @@ class ElementTree: """ def __init__(self, element=None, file=None): - # assert element is None or iselement(element) if element is not None and not iselement(element): raise TypeError(f"element must be etree.Element, " f"not {type(element).__name__}") @@ -546,7 +545,6 @@ def _setroot(self, element): with the given element. Use with care! """ - # assert iselement(element) if not iselement(element): raise TypeError self._root = element From 06eaff5e193aab8c8021207b9a4e83696fdfacc5 Mon Sep 17 00:00:00 2001 From: Kira Kaviani Date: Tue, 17 Jun 2025 15:24:22 -0400 Subject: [PATCH 03/18] Add tests for ElementTree constructor Add tests verifying the new type checking behavior --- Lib/test/test_xml_etree.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 38be2cd437f200..d1161c0b4a2b44 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -247,6 +247,13 @@ def check_element(element): self.assertRegex(repr(element), r"^$") element = ET.Element("tag", key="value") + # Verify type checking for ElementTree constructor + + with self.assertRaises(TypeError): + tree = ET.ElementTree("") + with self.assertRaises(TypeError): + tree = ET.ElementTree(ET.ElementTree()) + # Make sure all standard element methods exist. def check_method(method): From 6d4d65f335f57b00110668ac9088f8dce39dab20 Mon Sep 17 00:00:00 2001 From: Kira K Date: Tue, 17 Jun 2025 16:26:14 -0400 Subject: [PATCH 04/18] Add appropriate text to an exception --- Lib/xml/etree/ElementTree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 9a2d638fa15c37..4dabb211d0b863 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -546,7 +546,8 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError + raise TypeError(f"element must be etree.Element, " + f"not {type(element).__name__}") self._root = element def parse(self, source, parser=None): From 84b22d4aa60eab31e28ca5a94be12aabb6a2face Mon Sep 17 00:00:00 2001 From: Kira K Date: Sat, 21 Jun 2025 23:47:56 -0400 Subject: [PATCH 05/18] Make iselement stricter + incorporate feedback Adjusts iselement() based off feedback from picnixz. Since ElementTree needs to be compatible with element-like objects, the function now checks for all attributes that are necessary to instantiate an ElementTree object and successfully write it to a file (preventing issue #135640). This commit also incorporates other feedback requesting moving the tests and adding more tests, and adds a news entry. --- Lib/test/test_xml_etree.py | 43 ++++++++++++++++--- Lib/xml/etree/ElementTree.py | 9 ++-- ...-06-22-02-16-17.gh-issue-135640.FXyFL6.rst | 3 ++ 3 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index d1161c0b4a2b44..4d0acc7fc04bb6 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -218,6 +218,42 @@ class ElementTreeTest(unittest.TestCase): def serialize_check(self, elem, expected): self.assertEqual(serialize(elem), expected) + def test_constructor(self): + # Test constructor behavior. + + with self.assertRaises(TypeError): + tree = ET.ElementTree("") + with self.assertRaises(TypeError): + tree = ET.ElementTree(ET.ElementTree()) + + # Test _setroot as well, since it also sets the _root object. + + tree = ET.ElementTree() + with self.assertRaises(TypeError): + tree._setroot("") + with self.assertRaises(TypeError): + tree._setroot(ET.ElementTree()) + + # Make sure it accepts an Element-like object. + + class ElementLike: + def __init__(self): + self.tag = "tag" + self.text = None + self.tail = None + def iter(self): + pass + def items(self): + pass + def __len__(self): + pass + + element_like = ElementLike() + try: + tree = ET.ElementTree(element_like) + except Exception as err: + self.fail(err) + def test_interface(self): # Test element tree interface. @@ -247,13 +283,6 @@ def check_element(element): self.assertRegex(repr(element), r"^$") element = ET.Element("tag", key="value") - # Verify type checking for ElementTree constructor - - with self.assertRaises(TypeError): - tree = ET.ElementTree("") - with self.assertRaises(TypeError): - tree = ET.ElementTree(ET.ElementTree()) - # Make sure all standard element methods exist. def check_method(method): diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 4dabb211d0b863..4bef7d5710b7c0 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -120,8 +120,9 @@ class ParseError(SyntaxError): def iselement(element): """Return True if *element* appears to be an Element.""" - return hasattr(element, 'tag') - + return (hasattr(element, 'tag') and hasattr(element, 'text') and + hasattr(element, 'tail') and callable(element.iter) and + callable(element.items) and callable(element.__len__)) class Element: """An XML element. @@ -528,7 +529,7 @@ class ElementTree: """ def __init__(self, element=None, file=None): if element is not None and not iselement(element): - raise TypeError(f"element must be etree.Element, " + raise TypeError(f"element must be xml.etree.Element, " f"not {type(element).__name__}") self._root = element # first node if file: @@ -546,7 +547,7 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError(f"element must be etree.Element, " + raise TypeError(f"element must be xml.etree.Element, " f"not {type(element).__name__}") self._root = element diff --git a/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst b/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst new file mode 100644 index 00000000000000..49d1c5fead8843 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst @@ -0,0 +1,3 @@ +Address bug where calling :func:`xml.etree.ElementTree.ElementTree.write` on +an ElementTree object with an invalid root element would blank the file +passed to ``write`` if it already existed. From 1d2f0d94bd08f487832da8e7396be0db9883a94e Mon Sep 17 00:00:00 2001 From: Kira K Date: Thu, 26 Jun 2025 13:59:39 -0400 Subject: [PATCH 06/18] Revert iselement to previous state --- Lib/xml/etree/ElementTree.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 4bef7d5710b7c0..b417aefb743878 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -120,9 +120,7 @@ class ParseError(SyntaxError): def iselement(element): """Return True if *element* appears to be an Element.""" - return (hasattr(element, 'tag') and hasattr(element, 'text') and - hasattr(element, 'tail') and callable(element.iter) and - callable(element.items) and callable(element.__len__)) + return hasattr(element, 'tag') class Element: """An XML element. From d5bbe2358df504680e50b759659e3f18ee7d89ae Mon Sep 17 00:00:00 2001 From: Kira K Date: Thu, 26 Jun 2025 14:00:04 -0400 Subject: [PATCH 07/18] Add iselement check to ElementTree.write --- Lib/xml/etree/ElementTree.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index b417aefb743878..c12640c974d501 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -712,6 +712,9 @@ def write(self, file_or_filename, of start/end tags """ + if not iselement(self._root): + raise TypeError(f"Root element must be of type xml.etree.Element " + f"or Element-like object") if not method: method = "xml" elif method not in _serialize: From 4e2f822fefbc52805b658deae47fee5059f30f3c Mon Sep 17 00:00:00 2001 From: Kira K Date: Thu, 26 Jun 2025 14:02:26 -0400 Subject: [PATCH 08/18] Make errors more descriptive --- Lib/xml/etree/ElementTree.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index c12640c974d501..4e432477233b76 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -527,8 +527,9 @@ class ElementTree: """ def __init__(self, element=None, file=None): if element is not None and not iselement(element): - raise TypeError(f"element must be xml.etree.Element, " - f"not {type(element).__name__}") + raise TypeError(f"element must be xml.etree.Element or " + f"Element-like object, not " + f"{type(element).__name__}") self._root = element # first node if file: self.parse(file) @@ -545,8 +546,9 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError(f"element must be xml.etree.Element, " - f"not {type(element).__name__}") + raise TypeError(f"element must be xml.etree.Element or " + f"Element-like object, not " + f"{type(element).__name__}") self._root = element def parse(self, source, parser=None): From 8b588f354984a204d9130d85cd08bb32ca3ef415 Mon Sep 17 00:00:00 2001 From: Kira Date: Thu, 26 Jun 2025 15:22:28 -0400 Subject: [PATCH 09/18] Fix whitespace Co-authored-by: Mikhail Efimov --- Lib/xml/etree/ElementTree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 4e432477233b76..18396b8fac977a 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -122,6 +122,7 @@ def iselement(element): """Return True if *element* appears to be an Element.""" return hasattr(element, 'tag') + class Element: """An XML element. From e1e489e7f9765527a06c6a7d6f738a961fcfb0b6 Mon Sep 17 00:00:00 2001 From: Kira K Date: Thu, 26 Jun 2025 15:33:54 -0400 Subject: [PATCH 10/18] Rephrase news entry --- .../Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst b/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst index 49d1c5fead8843..ad217b57b4b510 100644 --- a/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst +++ b/Misc/NEWS.d/next/Library/2025-06-22-02-16-17.gh-issue-135640.FXyFL6.rst @@ -1,3 +1,4 @@ -Address bug where calling :func:`xml.etree.ElementTree.ElementTree.write` on -an ElementTree object with an invalid root element would blank the file -passed to ``write`` if it already existed. +Address bug where it was possible to call +:func:`xml.etree.ElementTree.ElementTree.write` on an ElementTree object with +an invalid root element. This behavior blanked the file passed to ``write`` +if it already existed. From 28fcc6956f923883a64880dbbcc54c3b58270983 Mon Sep 17 00:00:00 2001 From: Kira K Date: Thu, 26 Jun 2025 16:03:35 -0400 Subject: [PATCH 11/18] Test: remove unnecessary fields for element-like Test was written for a change to `iselement` that has since been undone --- Lib/test/test_xml_etree.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 4d0acc7fc04bb6..285be57a8ea553 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -239,14 +239,6 @@ def test_constructor(self): class ElementLike: def __init__(self): self.tag = "tag" - self.text = None - self.tail = None - def iter(self): - pass - def items(self): - pass - def __len__(self): - pass element_like = ElementLike() try: From 78118ea19bc0a715aeeb0bbc9f310da61e5d56bd Mon Sep 17 00:00:00 2001 From: Kira K Date: Fri, 27 Jun 2025 12:46:33 -0400 Subject: [PATCH 12/18] Change error messages --- Lib/xml/etree/ElementTree.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 18396b8fac977a..a302d4faaab2dd 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -528,7 +528,7 @@ class ElementTree: """ def __init__(self, element=None, file=None): if element is not None and not iselement(element): - raise TypeError(f"element must be xml.etree.Element or " + raise TypeError(f"expected an xml.etree.ElementTree.Element or " f"Element-like object, not " f"{type(element).__name__}") self._root = element # first node @@ -547,7 +547,7 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError(f"element must be xml.etree.Element or " + raise TypeError(f"expected an xml.etree.ElementTree.Element or " f"Element-like object, not " f"{type(element).__name__}") self._root = element @@ -716,7 +716,8 @@ def write(self, file_or_filename, """ if not iselement(self._root): - raise TypeError(f"Root element must be of type xml.etree.Element " + raise TypeError(f"Root element must be " + f"xml.etree.ElementTree.Element " f"or Element-like object") if not method: method = "xml" From bfd94cbe33bc52754f1bf936bc66a1de14195f63 Mon Sep 17 00:00:00 2001 From: Kira Date: Wed, 2 Jul 2025 20:56:22 -0400 Subject: [PATCH 13/18] Correct error messages Suggested changes from @serhiy-storchaka Co-authored-by: Serhiy Storchaka --- Lib/xml/etree/ElementTree.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index a302d4faaab2dd..33be029fa745e5 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -528,9 +528,7 @@ class ElementTree: """ def __init__(self, element=None, file=None): if element is not None and not iselement(element): - raise TypeError(f"expected an xml.etree.ElementTree.Element or " - f"Element-like object, not " - f"{type(element).__name__}") + raise TypeError('expected an Element, not %s' % type(e).__name__) self._root = element # first node if file: self.parse(file) @@ -547,9 +545,7 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError(f"expected an xml.etree.ElementTree.Element or " - f"Element-like object, not " - f"{type(element).__name__}") + raise TypeError('expected an Element, not %s' % type(e).__name__) self._root = element def parse(self, source, parser=None): From 7c3a3b25c3c45fd17ef96c7164b5d14feeb4c322 Mon Sep 17 00:00:00 2001 From: Kira Date: Wed, 2 Jul 2025 20:57:26 -0400 Subject: [PATCH 14/18] Correct error message Co-authored-by: Serhiy Storchaka --- Lib/xml/etree/ElementTree.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 33be029fa745e5..1292565a558d29 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -711,10 +711,8 @@ def write(self, file_or_filename, of start/end tags """ - if not iselement(self._root): - raise TypeError(f"Root element must be " - f"xml.etree.ElementTree.Element " - f"or Element-like object") + if self._root is None: + raise TypeError('ElementTree not initialized') if not method: method = "xml" elif method not in _serialize: From 97d5fa00860b7c9363341cfbf145889bd464cfe8 Mon Sep 17 00:00:00 2001 From: Kira K Date: Wed, 2 Jul 2025 21:49:10 -0400 Subject: [PATCH 15/18] Tests for _setroot Split tests for _setroot into their own function and remove test for element-like objects per feedback from @serhiy-storchaka --- Lib/test/test_xml_etree.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 285be57a8ea553..bf6d5074fdebd8 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -226,25 +226,24 @@ def test_constructor(self): with self.assertRaises(TypeError): tree = ET.ElementTree(ET.ElementTree()) - # Test _setroot as well, since it also sets the _root object. + def test_setroot(self): + # Test _setroot behavior. + + tree = ET.ElementTree() + element = ET.Element("tag") + tree._setroot(element) + self.assertEqual(tree.getroot().tag, "tag") + self.assertEqual(tree.getroot(), element) + + # Test behavior with an invalid root element tree = ET.ElementTree() with self.assertRaises(TypeError): tree._setroot("") with self.assertRaises(TypeError): tree._setroot(ET.ElementTree()) - - # Make sure it accepts an Element-like object. - - class ElementLike: - def __init__(self): - self.tag = "tag" - - element_like = ElementLike() - try: - tree = ET.ElementTree(element_like) - except Exception as err: - self.fail(err) + with self.assertRaises(TypeError): + tree._setroot(None) def test_interface(self): # Test element tree interface. From c3fb864ac192df18bcb02cc96211b5e04982b375 Mon Sep 17 00:00:00 2001 From: Kira K Date: Wed, 2 Jul 2025 21:51:50 -0400 Subject: [PATCH 16/18] Fix names --- Lib/xml/etree/ElementTree.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 1292565a558d29..dafe5b1b8a0c3f 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -528,7 +528,8 @@ class ElementTree: """ def __init__(self, element=None, file=None): if element is not None and not iselement(element): - raise TypeError('expected an Element, not %s' % type(e).__name__) + raise TypeError('expected an Element, not %s' % + type(element).__name__) self._root = element # first node if file: self.parse(file) @@ -545,7 +546,8 @@ def _setroot(self, element): """ if not iselement(element): - raise TypeError('expected an Element, not %s' % type(e).__name__) + raise TypeError('expected an Element, not %s' + % type(element).__name__) self._root = element def parse(self, source, parser=None): From d819ab6f863a39e41c5c9d3d78d242712eb99958 Mon Sep 17 00:00:00 2001 From: Kira K Date: Wed, 9 Jul 2025 14:57:55 -0400 Subject: [PATCH 17/18] Document iselement behavior when used with types --- Doc/library/xml.etree.elementtree.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 1daf6628013bf0..f6f4be84b50367 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -611,8 +611,11 @@ Functions .. function:: iselement(element) - Check if an object appears to be a valid element object. *element* is an - element instance. Return ``True`` if this is an element object. + Check if *element* appears to be a valid element object or type. Return + ``True`` if this is an element object or type. + + Because ``iselement`` behaves identically for both objects and types, code + requiring an object should check for this, see :func:`iselement`. .. function:: iterparse(source, events=None, parser=None) From ca17932438903065b61d3bc51936b3dab21a011e Mon Sep 17 00:00:00 2001 From: Kira K Date: Wed, 9 Jul 2025 15:06:46 -0400 Subject: [PATCH 18/18] News entry --- .../2025-07-09-15-06-27.gh-issue-136231.Wx8W-w.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2025-07-09-15-06-27.gh-issue-136231.Wx8W-w.rst diff --git a/Misc/NEWS.d/next/Documentation/2025-07-09-15-06-27.gh-issue-136231.Wx8W-w.rst b/Misc/NEWS.d/next/Documentation/2025-07-09-15-06-27.gh-issue-136231.Wx8W-w.rst new file mode 100644 index 00000000000000..d5082eaf48158f --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2025-07-09-15-06-27.gh-issue-136231.Wx8W-w.rst @@ -0,0 +1,2 @@ +Document that :func:`xml.etree.ElementTree.iselement` works identically on +both object instances and types.