From e1a82da5f5c69be1bb1dfbc61b31de6e36ec3beab06c8038e7150c7a54407ba3 Mon Sep 17 00:00:00 2001 From: Steve Kowalik Date: Wed, 19 Nov 2025 04:36:42 +0000 Subject: [PATCH] - Add patch support-python314.patch: * Support Python 3.14 changes. OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-et_xmlfile?expand=0&rev=17 --- .gitattributes | 23 ++ .gitignore | 1 + et_xmlfile-2.0.0.tar.gz | 3 + python-et_xmlfile.changes | 76 ++++ python-et_xmlfile.spec | 66 ++++ support-python314.patch | 787 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 956 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 et_xmlfile-2.0.0.tar.gz create mode 100644 python-et_xmlfile.changes create mode 100644 python-et_xmlfile.spec create mode 100644 support-python314.patch diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9b03811 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +## Default LFS +*.7z filter=lfs diff=lfs merge=lfs -text +*.bsp filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.gem filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.jar filter=lfs diff=lfs merge=lfs -text +*.lz filter=lfs diff=lfs merge=lfs -text +*.lzma filter=lfs diff=lfs merge=lfs -text +*.obscpio filter=lfs diff=lfs merge=lfs -text +*.oxt filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.rpm filter=lfs diff=lfs merge=lfs -text +*.tbz filter=lfs diff=lfs merge=lfs -text +*.tbz2 filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.txz filter=lfs diff=lfs merge=lfs -text +*.whl filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57affb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.osc diff --git a/et_xmlfile-2.0.0.tar.gz b/et_xmlfile-2.0.0.tar.gz new file mode 100644 index 0000000..b63e15a --- /dev/null +++ b/et_xmlfile-2.0.0.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b710736febf4b4589805f1cc0ccea41781353fc4550466c472649f8518bed4b5 +size 99140 diff --git a/python-et_xmlfile.changes b/python-et_xmlfile.changes new file mode 100644 index 0000000..578b434 --- /dev/null +++ b/python-et_xmlfile.changes @@ -0,0 +1,76 @@ +------------------------------------------------------------------- +Wed Nov 19 04:36:02 UTC 2025 - Steve Kowalik + +- Add patch support-python314.patch: + * Support Python 3.14 changes. + +------------------------------------------------------------------- +Sat Nov 23 18:10:21 UTC 2024 - Dirk Müller + +- update to 2.0.0: + * Add new writer method and namespace Element / parsing + * Readd the Element with namespaces + * Add el_has_namespaces to ElementTree classes + * Deprecate python 3.6 and 3.7 as nsetree requires a more recent XMLParser + * Fix docstrings of _IncrementalFileWriter + +------------------------------------------------------------------- +Thu Sep 26 18:03:11 UTC 2024 - Guang Yee + +- Enable sle15_python_module_pythons. + +------------------------------------------------------------------- +Tue May 23 13:14:19 UTC 2023 - Matej Cepl + +- Clean up the SPEC file. + +------------------------------------------------------------------- +Tue May 23 03:54:32 UTC 2023 - Jiri Srain + +- updated the LICENSE.rst URL + +------------------------------------------------------------------- +Thu Mar 12 07:19:24 UTC 2020 - Tomáš Chvátal + +- Make sure to run without python2 + +------------------------------------------------------------------- +Thu Dec 6 13:25:04 UTC 2018 - Tomáš Chvátal + +- Run the tests +- Make sure to run fudpes correctly + +------------------------------------------------------------------- +Tue Dec 4 12:47:44 UTC 2018 - Matej Cepl + +- Remove superfluous devel dependency for noarch package + +------------------------------------------------------------------- +Wed Jul 26 08:56:00 UTC 2017 - bruno@ioda-net.ch + +- specfile : add line to define python-modules (fix build on Leap) + +------------------------------------------------------------------- +Wed Jun 7 12:56:15 UTC 2017 - bruno@ioda-net.ch + +- Move to singlespec +- Add missing requires python-lxml +- Prepare everything for testing (wip) + +------------------------------------------------------------------- +Thu Jul 7 16:16:51 UTC 2016 - toddrme2178@gmail.com + +- Requires full python3 package. + +------------------------------------------------------------------- +Sun May 8 07:14:53 UTC 2016 - arun@gmx.de + +- specfile: + * updated source url to files.pythonhosted.org + + +------------------------------------------------------------------- +Thu Nov 19 16:53:37 UTC 2015 - bruno@ioda-net.ch + +- Initial Release : build python3 + diff --git a/python-et_xmlfile.spec b/python-et_xmlfile.spec new file mode 100644 index 0000000..cbfb96d --- /dev/null +++ b/python-et_xmlfile.spec @@ -0,0 +1,66 @@ +# +# spec file for package python-et_xmlfile +# +# Copyright (c) 2025 SUSE LLC and contributors +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via https://bugs.opensuse.org/ +# + + +%{?sle15_python_module_pythons} +Name: python-et_xmlfile +Version: 2.0.0 +Release: 0 +Summary: An implementation of lxml.xmlfile for the standard library +License: MIT +URL: https://foss.heptapod.net/openpyxl/et_xmlfile +Source0: https://foss.heptapod.net/openpyxl/et_xmlfile/-/archive/%{version}/et_xmlfile-%{version}.tar.gz +# PATCH-FIX-UPSTREAM https://foss.heptapod.net/openpyxl/et_xmlfile/-/merge_requests/4 +Patch0: support-python314.patch +BuildRequires: %{python_module lxml} +BuildRequires: %{python_module pip} +BuildRequires: %{python_module pytest} +BuildRequires: %{python_module wheel} +BuildRequires: fdupes +BuildRequires: python-rpm-macros +Requires: python-jdcal +Requires: python-lxml >= 3.4 +BuildArch: noarch +%python_subpackages + +%description +et_xmlfile is a low memory library for creating large XML files. + +It is based upon the xmlfile module from lxml with the aim of allowing code to +be developed that will work with both libraries. It was developed initially for +the openpyxl project but is now a standalone module. + +%prep +%autosetup -p1 -n et_xmlfile-%{version} + +%build +%pyproject_wheel + +%install +%pyproject_install +%python_expand %fdupes %{buildroot}%{$python_sitelib} + +%check +%pytest et_xmlfile/tests + +%files %{python_files} +%license LICENCE.rst +%doc README.rst +%{python_sitelib}/et_xmlfile-%{version}.dist-info +%{python_sitelib}/et_xmlfile + +%changelog diff --git a/support-python314.patch b/support-python314.patch new file mode 100644 index 0000000..7cb6582 --- /dev/null +++ b/support-python314.patch @@ -0,0 +1,787 @@ +# HG changeset patch +# User Daniel Hillier +# Date 1760398968 -39600 +# Tue Oct 14 10:42:48 2025 +1100 +# Branch 2.0 +# Node ID 73172a7ce6d819ce13e6706f9a1c6d50f1646dde +# Parent 7bf584f2b7fff95565483a40a04e64a0a4951cdc +Update stdlib tests to py3.14.0 + +diff --git a/et_xmlfile/tests/_vendor/test/test_xml_etree.py b/et_xmlfile/tests/_vendor/test/test_xml_etree.py +--- a/et_xmlfile/tests/_vendor/test/test_xml_etree.py ++++ b/et_xmlfile/tests/_vendor/test/test_xml_etree.py +@@ -18,17 +18,21 @@ + import textwrap + import types + import unittest ++import unittest.mock as mock + import warnings + import weakref + ++from contextlib import nullcontext + from functools import partial + from itertools import product, islice ++# et_xmlfile change: make test imports relative to vendored modules + from . import support + from .support import os_helper + from .support import warnings_helper + from .support import findfile, gc_collect, swap_attr, swap_item + from .support.import_helper import import_fresh_module + from .support.os_helper import TESTFN ++# end et_xmlfile change + + + # pyET is the pure-Python implementation. +@@ -121,6 +125,21 @@ + + """ + ++def is_python_implementation(): ++ assert ET is not None, "ET must be initialized" ++ assert pyET is not None, "pyET must be initialized" ++ return ET is pyET ++ ++ ++def equal_wrapper(cls): ++ """Mock cls.__eq__ to check whether it has been called or not. ++ ++ The behaviour of cls.__eq__ (side-effects included) is left as is. ++ """ ++ eq = cls.__eq__ ++ return mock.patch.object(cls, "__eq__", autospec=True, wraps=eq) ++ ++ + def checkwarnings(*filters, quiet=False): + def decorator(test): + def newtest(*args, **kwargs): +@@ -201,6 +220,33 @@ + 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()) ++ ++ 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()) ++ with self.assertRaises(TypeError): ++ tree._setroot(None) ++ + def test_interface(self): + # Test element tree interface. + +@@ -208,8 +254,7 @@ + self.assertTrue(ET.iselement(element), msg="not an element") + direlem = dir(element) + for attr in 'tag', 'attrib', 'text', 'tail': +- self.assertTrue(hasattr(element, attr), +- msg='no %s member' % attr) ++ self.assertHasAttr(element, attr) + self.assertIn(attr, direlem, + msg='no %s visible by dir' % attr) + +@@ -234,7 +279,7 @@ + # Make sure all standard element methods exist. + + def check_method(method): +- self.assertTrue(hasattr(method, '__call__'), ++ self.assertHasAttr(method, '__call__', + msg="%s not callable" % method) + + check_method(element.append) +@@ -327,9 +372,9 @@ + self.serialize_check(element, '') # 4 + element.remove(subelement) + self.serialize_check(element, '') # 5 +- with self.assertRaises(ValueError) as cm: ++ with self.assertRaisesRegex(ValueError, ++ r'Element\.remove\(.+\): element not found'): + element.remove(subelement) +- self.assertEqual(str(cm.exception), 'list.remove(x): x not in list') + self.serialize_check(element, '') # 6 + element[0:0] = [subelement, subelement, subelement] + self.serialize_check(element[1], '') +@@ -2642,6 +2687,7 @@ + + + class BadElementTest(ElementTestCase, unittest.TestCase): ++ + def test_extend_mutable_list(self): + class X: + @property +@@ -2680,18 +2726,168 @@ + e = ET.Element('foo') + e.extend(L) + +- def test_remove_with_mutating(self): +- class X(ET.Element): ++ def test_remove_with_clear_assume_missing(self): ++ # gh-126033: Check that a concurrent clear() for an assumed-to-be ++ # missing element does not make the interpreter crash. ++ self.do_test_remove_with_clear(raises=True) ++ ++ def test_remove_with_clear_assume_existing(self): ++ # gh-126033: Check that a concurrent clear() for an assumed-to-be ++ # existing element does not make the interpreter crash. ++ self.do_test_remove_with_clear(raises=False) ++ ++ def do_test_remove_with_clear(self, *, raises): ++ ++ # Until the discrepency between "del root[:]" and "root.clear()" is ++ # resolved, we need to keep two tests. Previously, using "del root[:]" ++ # did not crash with the reproducer of gh-126033 while "root.clear()" ++ # did. ++ ++ class E(ET.Element): ++ """Local class to be able to mock E.__eq__ for introspection.""" ++ ++ class X(E): ++ def __eq__(self, o): ++ del root[:] ++ return not raises ++ ++ class Y(E): + def __eq__(self, o): +- del e[:] +- return False +- e = ET.Element('foo') +- e.extend([X('bar')]) +- self.assertRaises(ValueError, e.remove, ET.Element('baz')) +- +- e = ET.Element('foo') +- e.extend([ET.Element('bar')]) +- self.assertRaises(ValueError, e.remove, X('baz')) ++ root.clear() ++ return not raises ++ ++ if raises: ++ get_checker_context = lambda: self.assertRaises(ValueError) ++ else: ++ get_checker_context = nullcontext ++ ++ self.assertIs(E.__eq__, object.__eq__) ++ ++ for Z, side_effect in [(X, 'del root[:]'), (Y, 'root.clear()')]: ++ self.enterContext(self.subTest(side_effect=side_effect)) ++ ++ # test removing R() from [U()] ++ for R, U, description in [ ++ (E, Z, "remove missing E() from [Z()]"), ++ (Z, E, "remove missing Z() from [E()]"), ++ (Z, Z, "remove missing Z() from [Z()]"), ++ ]: ++ with self.subTest(description): ++ root = E('top') ++ root.extend([U('one')]) ++ with get_checker_context(): ++ root.remove(R('missing')) ++ ++ # test removing R() from [U(), V()] ++ cases = self.cases_for_remove_missing_with_mutations(E, Z) ++ for R, U, V, description in cases: ++ with self.subTest(description): ++ root = E('top') ++ root.extend([U('one'), V('two')]) ++ with get_checker_context(): ++ root.remove(R('missing')) ++ ++ # Test removing root[0] from [Z()]. ++ # ++ # Since we call root.remove() with root[0], Z.__eq__() ++ # will not be called (we branch on the fast Py_EQ path). ++ with self.subTest("remove root[0] from [Z()]"): ++ root = E('top') ++ root.append(Z('rem')) ++ with equal_wrapper(E) as f, equal_wrapper(Z) as g: ++ root.remove(root[0]) ++ f.assert_not_called() ++ g.assert_not_called() ++ ++ # Test removing root[1] (of type R) from [U(), R()]. ++ is_special = is_python_implementation() and raises and Z is Y ++ if is_python_implementation() and raises and Z is Y: ++ # In pure Python, using root.clear() sets the children ++ # list to [] without calling list.clear(). ++ # ++ # For this reason, the call to root.remove() first ++ # checks root[0] and sets the children list to [] ++ # since either root[0] or root[1] is an evil element. ++ # ++ # Since checking root[1] still uses the old reference ++ # to the children list, PyObject_RichCompareBool() branches ++ # to the fast Py_EQ path and Y.__eq__() is called exactly ++ # once (when checking root[0]). ++ continue ++ else: ++ cases = self.cases_for_remove_existing_with_mutations(E, Z) ++ for R, U, description in cases: ++ with self.subTest(description): ++ root = E('top') ++ root.extend([U('one'), R('rem')]) ++ with get_checker_context(): ++ root.remove(root[1]) ++ ++ def test_remove_with_mutate_root_assume_missing(self): ++ # gh-126033: Check that a concurrent mutation for an assumed-to-be ++ # missing element does not make the interpreter crash. ++ self.do_test_remove_with_mutate_root(raises=True) ++ ++ def test_remove_with_mutate_root_assume_existing(self): ++ # gh-126033: Check that a concurrent mutation for an assumed-to-be ++ # existing element does not make the interpreter crash. ++ self.do_test_remove_with_mutate_root(raises=False) ++ ++ def do_test_remove_with_mutate_root(self, *, raises): ++ E = ET.Element ++ ++ class Z(E): ++ def __eq__(self, o): ++ del root[0] ++ return not raises ++ ++ if raises: ++ get_checker_context = lambda: self.assertRaises(ValueError) ++ else: ++ get_checker_context = nullcontext ++ ++ # test removing R() from [U(), V()] ++ cases = self.cases_for_remove_missing_with_mutations(E, Z) ++ for R, U, V, description in cases: ++ with self.subTest(description): ++ root = E('top') ++ root.extend([U('one'), V('two')]) ++ with get_checker_context(): ++ root.remove(R('missing')) ++ ++ # test removing root[1] (of type R) from [U(), R()] ++ cases = self.cases_for_remove_existing_with_mutations(E, Z) ++ for R, U, description in cases: ++ with self.subTest(description): ++ root = E('top') ++ root.extend([U('one'), R('rem')]) ++ with get_checker_context(): ++ root.remove(root[1]) ++ ++ def cases_for_remove_missing_with_mutations(self, E, Z): ++ # Cases for removing R() from [U(), V()]. ++ # The case U = V = R = E is not interesting as there is no mutation. ++ for U, V in [(E, Z), (Z, E), (Z, Z)]: ++ description = (f"remove missing {E.__name__}() from " ++ f"[{U.__name__}(), {V.__name__}()]") ++ yield E, U, V, description ++ ++ for U, V in [(E, E), (E, Z), (Z, E), (Z, Z)]: ++ description = (f"remove missing {Z.__name__}() from " ++ f"[{U.__name__}(), {V.__name__}()]") ++ yield Z, U, V, description ++ ++ def cases_for_remove_existing_with_mutations(self, E, Z): ++ # Cases for removing root[1] (of type R) from [U(), R()]. ++ # The case U = R = E is not interesting as there is no mutation. ++ for U, R, description in [ ++ (E, Z, "remove root[1] from [E(), Z()]"), ++ (Z, E, "remove root[1] from [Z(), E()]"), ++ (Z, Z, "remove root[1] from [Z(), Z()]"), ++ ]: ++ description = (f"remove root[1] (of type {R.__name__}) " ++ f"from [{U.__name__}(), {R.__name__}()]") ++ yield R, U, description + + @support.infinite_recursion(25) + def test_recursive_repr(self): +@@ -2792,21 +2988,83 @@ + del b + gc_collect() + +- +-class MutatingElementPath(str): ++ def test_deepcopy_clear(self): ++ # Prevent crashes when __deepcopy__() clears the children list. ++ # See https://github.com/python/cpython/issues/133009. ++ class X(ET.Element): ++ def __deepcopy__(self, memo): ++ root.clear() ++ return self ++ ++ root = ET.Element('a') ++ evil = X('x') ++ root.extend([evil, ET.Element('y')]) ++ if is_python_implementation(): ++ # Mutating a list over which we iterate raises an error. ++ self.assertRaises(RuntimeError, copy.deepcopy, root) ++ else: ++ c = copy.deepcopy(root) ++ # In the C implementation, we can still copy the evil element. ++ self.assertListEqual(list(c), [evil]) ++ ++ def test_deepcopy_grow(self): ++ # Prevent crashes when __deepcopy__() mutates the children list. ++ # See https://github.com/python/cpython/issues/133009. ++ a = ET.Element('a') ++ b = ET.Element('b') ++ c = ET.Element('c') ++ ++ class X(ET.Element): ++ def __deepcopy__(self, memo): ++ root.append(a) ++ root.append(b) ++ return self ++ ++ root = ET.Element('top') ++ evil1, evil2 = X('1'), X('2') ++ root.extend([evil1, c, evil2]) ++ children = list(copy.deepcopy(root)) ++ # mock deep copies ++ self.assertIs(children[0], evil1) ++ self.assertIs(children[2], evil2) ++ # true deep copies ++ self.assertEqual(children[1].tag, c.tag) ++ self.assertEqual([c.tag for c in children[3:]], ++ [a.tag, b.tag, a.tag, b.tag]) ++ ++ ++class MutationDeleteElementPath(str): + def __new__(cls, elem, *args): + self = str.__new__(cls, *args) + self.elem = elem + return self ++ + def __eq__(self, o): + del self.elem[:] + return True +-MutatingElementPath.__hash__ = str.__hash__ ++ ++ __hash__ = str.__hash__ ++ ++ ++class MutationClearElementPath(str): ++ def __new__(cls, elem, *args): ++ self = str.__new__(cls, *args) ++ self.elem = elem ++ return self ++ ++ def __eq__(self, o): ++ self.elem.clear() ++ return True ++ ++ __hash__ = str.__hash__ ++ + + class BadElementPath(str): + def __eq__(self, o): + raise 1/0 +-BadElementPath.__hash__ = str.__hash__ ++ ++ __hash__ = str.__hash__ ++ + + class BadElementPathTest(ElementTestCase, unittest.TestCase): + def setUp(self): +@@ -2821,9 +3079,11 @@ + super().tearDown() + + def test_find_with_mutating(self): +- e = ET.Element('foo') +- e.extend([ET.Element('bar')]) +- e.find(MutatingElementPath(e, 'x')) ++ for cls in [MutationDeleteElementPath, MutationClearElementPath]: ++ with self.subTest(cls): ++ e = ET.Element('foo') ++ e.extend([ET.Element('bar')]) ++ e.find(cls(e, 'x')) + + def test_find_with_error(self): + e = ET.Element('foo') +@@ -2834,9 +3094,11 @@ + pass + + def test_findtext_with_mutating(self): +- e = ET.Element('foo') +- e.extend([ET.Element('bar')]) +- e.findtext(MutatingElementPath(e, 'x')) ++ for cls in [MutationDeleteElementPath, MutationClearElementPath]: ++ with self.subTest(cls): ++ e = ET.Element('foo') ++ e.extend([ET.Element('bar')]) ++ e.findtext(cls(e, 'x')) + + def test_findtext_with_error(self): + e = ET.Element('foo') +@@ -2861,9 +3123,11 @@ + self.assertEqual(root_elem.findtext('./bar'), '') + + def test_findall_with_mutating(self): +- e = ET.Element('foo') +- e.extend([ET.Element('bar')]) +- e.findall(MutatingElementPath(e, 'x')) ++ for cls in [MutationDeleteElementPath, MutationClearElementPath]: ++ with self.subTest(cls): ++ e = ET.Element('foo') ++ e.extend([ET.Element('bar')]) ++ e.findall(cls(e, 'x')) + + def test_findall_with_error(self): + e = ET.Element('foo') +@@ -4372,7 +4636,7 @@ + # When invoked without a module, runs the Python ET tests by loading pyET. + # Otherwise, uses the given module as the ET. + global pyET +- pyET = import_fresh_module(module.__name__, ++ pyET = import_fresh_module('xml.etree.ElementTree', + blocked=['_elementtree']) + if module is None: + module = pyET +diff --git a/et_xmlfile/tests/stdlib_base_tests.py b/et_xmlfile/tests/stdlib_base_tests.py +--- a/et_xmlfile/tests/stdlib_base_tests.py ++++ b/et_xmlfile/tests/stdlib_base_tests.py +@@ -1,6 +1,7 @@ + import io + import platform + import sys ++import types + import unittest + import unittest.case + +@@ -11,6 +12,18 @@ + old_serialize = test_xml_etree.serialize + + ++def is_version_before(*versions): ++ sys_ver = sys.version_info[:3] ++ for version in sorted(versions): ++ if sys_ver[:2] == version[:2]: ++ # Check for point release eg. (3, 12, 10) ++ if sys_ver < version: ++ return True ++ if sys_ver < min(versions): ++ return True ++ return False ++ ++ + def serialize(elem, **options): + if "root_ns_only" not in options: + options["root_ns_only"] = True +@@ -51,6 +64,88 @@ + + + class ElementTreeTest(test_xml_etree.ElementTreeTest): ++ if sys.version_info[:2] < (3, 14): ++ def assertHasAttr(self, obj, name, msg=None): ++ if not hasattr(obj, name): ++ if isinstance(obj, types.ModuleType): ++ standardMsg = f'module {obj.__name__!r} has no attribute {name!r}' ++ elif isinstance(obj, type): ++ standardMsg = f'type object {obj.__name__!r} has no attribute {name!r}' ++ else: ++ standardMsg = f'{type(obj).__name__!r} object has no attribute {name!r}' ++ self.fail(self._formatMessage(msg, standardMsg)) ++ ++ @unittest.skipIf( ++ sys.version_info[:2] < (3, 13, 6), ++ "Added in 3.13.6" ++ ) ++ def test_setroot(self): ++ super().test_setroot() ++ ++ @unittest.skipIf( ++ sys.version_info[:2] < (3, 13, 6), ++ "Added in 3.13.6" ++ ) ++ def test_constructor(self): ++ super().test_constructor() ++ ++ def _test_simpleops_pre_3_13(self): ++ # Basic method sanity checks. ++ ++ elem = test_xml_etree.ET.XML("") ++ self.serialize_check(elem, '') ++ e = test_xml_etree.ET.Element("tag2") ++ elem.append(e) ++ self.serialize_check(elem, '') ++ elem.remove(e) ++ self.serialize_check(elem, '') ++ elem.insert(0, e) ++ self.serialize_check(elem, '') ++ elem.remove(e) ++ elem.extend([e]) ++ self.serialize_check(elem, '') ++ elem.remove(e) ++ elem.extend(iter([e])) ++ self.serialize_check(elem, '') ++ elem.remove(e) ++ ++ element = test_xml_etree.ET.Element("tag", key="value") ++ self.serialize_check(element, '') # 1 ++ subelement = test_xml_etree.ET.Element("subtag") ++ element.append(subelement) ++ self.serialize_check(element, '') # 2 ++ element.insert(0, subelement) ++ self.serialize_check(element, ++ '') # 3 ++ element.remove(subelement) ++ self.serialize_check(element, '') # 4 ++ element.remove(subelement) ++ self.serialize_check(element, '') # 5 ++ with self.assertRaises(ValueError) as cm: ++ element.remove(subelement) ++ self.assertEqual(str(cm.exception), 'list.remove(x): x not in list') ++ self.serialize_check(element, '') # 6 ++ element[0:0] = [subelement, subelement, subelement] ++ self.serialize_check(element[1], '') ++ self.assertEqual(element[1:9], [element[1], element[2]]) ++ self.assertEqual(element[:9:2], [element[0], element[2]]) ++ del element[1:2] ++ self.serialize_check(element, ++ '') ++ ++ @unittest.skipIf( ++ ( ++ platform.python_implementation() == "PyPy" ++ and sys.version_info[:3] < (3, 10, 15) ++ ), ++ "Functionality reverted but not picked up by PyPy yet", ++ ) ++ def test_simpleops(self): ++ if sys.version_info[:2] < (3, 14): ++ self._test_simpleops_pre_3_13() ++ else: ++ super().test_simpleops() ++ + def _test_iterparse_pre_3_13(self): + # Test iterparse interface. + +@@ -241,16 +336,6 @@ + def test_initialize_parser_without_target(self): + super().test_initialize_parser_without_target() + +- @unittest.skipIf( +- ( +- platform.python_implementation() == "PyPy" +- and sys.version_info[:3] < (3, 10, 15) +- ), +- "Functionality reverted but not picked up by PyPy yet", +- ) +- def test_simpleops(self): +- super().test_simpleops() +- + + class BasicElementTest(test_xml_etree.BasicElementTest): + @unittest.skipIf( +@@ -291,6 +376,21 @@ + super().test_xinclude_repeated() + + ++# Need for _test_findall_with_mutating_pre_3_12_5_or_3_13_4 ++class MutatingElementPath(str): ++ def __new__(cls, elem, *args): ++ self = str.__new__(cls, *args) ++ self.elem = elem ++ return self ++ ++ def __eq__(self, o): ++ del self.elem[:] ++ return True ++ ++ ++MutatingElementPath.__hash__ = str.__hash__ ++ ++ + class BadElementPathTest(test_xml_etree.BadElementPathTest): + @unittest.skipIf( + sys.version_info[:2] < (3, 11), +@@ -299,6 +399,54 @@ + def test_findtext_with_falsey_text_attribute(self): + super().test_findtext_with_falsey_text_attribute() + ++ def _test_findall_with_mutating_pre_3_12_10_or_3_13_4(self): ++ e = test_xml_etree.ET.Element('foo') ++ e.extend([test_xml_etree.ET.Element('bar')]) ++ e.findall(MutatingElementPath(e, 'x')) ++ ++ def test_findall_with_mutating(self): ++ if is_version_before((3, 12, 10), (3, 13, 4)): ++ self._test_findall_with_mutating_pre_3_12_10_or_3_13_4() ++ else: ++ super().test_findall_with_mutating() ++ ++ ++class BadElementTest(test_xml_etree.BadElementTest): ++ @unittest.skipIf( ++ sys.version_info[:3] < (3, 13, 4), ++ "Crashes python before fix", ++ ) ++ def test_deepcopy_clear(self): ++ super().test_deepcopy_clear() ++ ++ @unittest.skipIf( ++ sys.version_info[:3] < (3, 13, 4), ++ "Crashes python before fix", ++ ) ++ def test_deepcopy_grow(self): ++ super().test_deepcopy_grow() ++ ++ @unittest.skipIf( ++ is_version_before((3, 12, 10), (3, 13, 4)), ++ "Only fixed in 3.12.10 and after", ++ ) ++ def test_remove_with_clear_assume_existing(self): ++ super().test_remove_with_clear_assume_existing() ++ ++ @unittest.skipIf( ++ is_version_before((3, 12, 10), (3, 13, 4)), ++ "Only fixed in 3.12.10 and after", ++ ) ++ def test_remove_with_clear_assume_missing(self): ++ super().test_remove_with_clear_assume_missing() ++ ++ @unittest.skipIf( ++ is_version_before((3, 12, 10), (3, 13, 4)), ++ "Only fixed in 3.12.10 and after", ++ ) ++ def test_remove_with_mutate_root_assume_existing(self): ++ super().test_remove_with_mutate_root_assume_existing() ++ + + class NoAcceleratorTest(test_xml_etree.NoAcceleratorTest): + @unittest.skipIf( +diff --git a/et_xmlfile/tests/test_incremental_tree_with_stdlib_tests.py b/et_xmlfile/tests/test_incremental_tree_with_stdlib_tests.py +--- a/et_xmlfile/tests/test_incremental_tree_with_stdlib_tests.py ++++ b/et_xmlfile/tests/test_incremental_tree_with_stdlib_tests.py +@@ -29,7 +29,7 @@ + if sys.version_info[:2] == (3, 10): + class IOTest(stdlib_base_tests.IOTest): + @unittest.skip( +- "Fixeb by: gh-91810: Fix regression with writing an XML declaration with encoding='unicode'" ++ "Fixed by: gh-91810: Fix regression with writing an XML declaration with encoding='unicode'" + ) + def test_write_to_text_file(self): + pass +diff --git a/et_xmlfile/tests/updating_stdlib_tests.rst b/et_xmlfile/tests/updating_stdlib_tests.rst +new file mode 100644 +--- /dev/null ++++ b/et_xmlfile/tests/updating_stdlib_tests.rst +@@ -0,0 +1,55 @@ ++====================== ++Updating stdlib tests ++====================== ++ ++The ``incremental_tree.py`` code extends many classes defined by Python's ++``xml.etree.ElementTree`` adding additional functionality with regards to how ++these trees are serialised. Serialising xml is not a trivial task so we ++leverage the standard library tests to take advantage of the ~4600 loc of tests ++to ensure the implementation in this package is working as expected. ++ ++An overview: ++ ++* We vendor the latest tests from a Python release in the the ``tests/_vendor`` ++ directory. ++* ``pytest`` is configured to ignore the tests in ``tests/_vendor`` so we can apply ++ some shims and workarounds to support mulitple versions of Pythons. ++* Modifications to the stdlib ``TestCase`` classes are created in subclasses of ++ the those TestCases in the ``tests/stdlib_base_tests.py`` file. This keeps ++ the vendored code clean to allow easy updates to newer releases of cPython. ++* The test runner will find these modified test cases via the ++ ``tests/test_incremental_tree_with_stdlib_tests.py`` file. ++ ++ ++# Updating the stdlib tests ++ ++As cPython implements new features and adds bug fixes, the snapshot of the ++tests we've vendored from the cPython project (under the ``tests/_vendor`` ++directory) may start to fail for more recent versions of cPython. ++ ++To update the vendored tests: ++ ++* Clone the cPython repository ++* Checkout the latest release tag. It's important it is a release tag so that ++ we don't include tests that aren't released yet as that may cause test ++ failures. ++* Copy the ``Lib/test/test_xml_etree.py`` file over the ++ ``tests/_vendor/teststest_xml_etree.py`` file in this repository. ++* Changes to the local ``teststest_xml_etree.py`` are kept to a minimum but there ++ are a few required modifications. They are surrounded by the comments: ++ ++ ``` ++ # et_xmlfile change: ... ++ ++ # end et_xmlfile change ++ ``` ++ ++ Check the hg diff after replacing the local ``test_xml_etree.py`` with the ++ newer version to find any of these sections that may have been removed and ++ readd them. ++* Run ``pytest`` for supported python versions. Look for test failures due to new ++ features or code changes and update the corresponding classes in ++ ``stdlib_base_tests.py`` to override the tests in ``test_xml_etree.py``. This can ++ mean copying the old version of a test and running that on older versions of ++ Python while retaining the newer test for the Pythons that support that. ++* Don't forget to check pypy :) +# HG changeset patch +# User Daniel Hillier +# Date 1760399026 -39600 +# Tue Oct 14 10:43:46 2025 +1100 +# Branch 2.0 +# Node ID e5fa3d4955d005e67a0d022a8732caf13ae65256 +# Parent 73172a7ce6d819ce13e6706f9a1c6d50f1646dde +Add 3.14 to supported classifier and tests + +diff --git a/setup.py b/setup.py +--- a/setup.py ++++ b/setup.py +@@ -56,5 +56,6 @@ + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ++ 'Programming Language :: Python :: 3.14', + ], + ) +diff --git a/tox.ini b/tox.ini +--- a/tox.ini ++++ b/tox.ini +@@ -11,6 +11,7 @@ + py311, + py312, + py313, ++ py314, + doc, + + [testenv] +# HG changeset patch +# User Daniel Hillier +# Date 1760400015 -39600 +# Tue Oct 14 11:00:15 2025 +1100 +# Branch 2.0 +# Node ID ab78a479af10bafd4267b74f92a44dffa1bc5320 +# Parent e5fa3d4955d005e67a0d022a8732caf13ae65256 +Update skip version for test still not supported by Pypy + +diff --git a/et_xmlfile/tests/stdlib_base_tests.py b/et_xmlfile/tests/stdlib_base_tests.py +--- a/et_xmlfile/tests/stdlib_base_tests.py ++++ b/et_xmlfile/tests/stdlib_base_tests.py +@@ -136,7 +136,7 @@ + @unittest.skipIf( + ( + platform.python_implementation() == "PyPy" +- and sys.version_info[:3] < (3, 10, 15) ++ and sys.version_info[:3] < (3, 12) + ), + "Functionality reverted but not picked up by PyPy yet", + )