forked from pool/python-et_xmlfile
- 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
This commit is contained in:
23
.gitattributes
vendored
Normal file
23
.gitattributes
vendored
Normal file
@@ -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
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.osc
|
||||
3
et_xmlfile-2.0.0.tar.gz
Normal file
3
et_xmlfile-2.0.0.tar.gz
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b710736febf4b4589805f1cc0ccea41781353fc4550466c472649f8518bed4b5
|
||||
size 99140
|
||||
76
python-et_xmlfile.changes
Normal file
76
python-et_xmlfile.changes
Normal file
@@ -0,0 +1,76 @@
|
||||
-------------------------------------------------------------------
|
||||
Wed Nov 19 04:36:02 UTC 2025 - Steve Kowalik <steven.kowalik@suse.com>
|
||||
|
||||
- Add patch support-python314.patch:
|
||||
* Support Python 3.14 changes.
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Sat Nov 23 18:10:21 UTC 2024 - Dirk Müller <dmueller@suse.com>
|
||||
|
||||
- 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 <gyee@suse.com>
|
||||
|
||||
- Enable sle15_python_module_pythons.
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Tue May 23 13:14:19 UTC 2023 - Matej Cepl <mcepl@suse.com>
|
||||
|
||||
- Clean up the SPEC file.
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Tue May 23 03:54:32 UTC 2023 - Jiri Srain <jsrain@suse.com>
|
||||
|
||||
- updated the LICENSE.rst URL
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Thu Mar 12 07:19:24 UTC 2020 - Tomáš Chvátal <tchvatal@suse.com>
|
||||
|
||||
- Make sure to run without python2
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Thu Dec 6 13:25:04 UTC 2018 - Tomáš Chvátal <tchvatal@suse.com>
|
||||
|
||||
- Run the tests
|
||||
- Make sure to run fudpes correctly
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Tue Dec 4 12:47:44 UTC 2018 - Matej Cepl <mcepl@suse.com>
|
||||
|
||||
- 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
|
||||
|
||||
66
python-et_xmlfile.spec
Normal file
66
python-et_xmlfile.spec
Normal file
@@ -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
|
||||
787
support-python314.patch
Normal file
787
support-python314.patch
Normal file
@@ -0,0 +1,787 @@
|
||||
# HG changeset patch
|
||||
# User Daniel Hillier <daniel.hillier@gmail.com>
|
||||
# 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 @@
|
||||
</foo>
|
||||
"""
|
||||
|
||||
+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, '<tag key="value"><subtag /></tag>') # 4
|
||||
element.remove(subelement)
|
||||
self.serialize_check(element, '<tag key="value" />') # 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, '<tag key="value" />') # 6
|
||||
element[0:0] = [subelement, subelement, subelement]
|
||||
self.serialize_check(element[1], '<subtag />')
|
||||
@@ -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("<body><tag/></body>")
|
||||
+ self.serialize_check(elem, '<body><tag /></body>')
|
||||
+ e = test_xml_etree.ET.Element("tag2")
|
||||
+ elem.append(e)
|
||||
+ self.serialize_check(elem, '<body><tag /><tag2 /></body>')
|
||||
+ elem.remove(e)
|
||||
+ self.serialize_check(elem, '<body><tag /></body>')
|
||||
+ elem.insert(0, e)
|
||||
+ self.serialize_check(elem, '<body><tag2 /><tag /></body>')
|
||||
+ elem.remove(e)
|
||||
+ elem.extend([e])
|
||||
+ self.serialize_check(elem, '<body><tag /><tag2 /></body>')
|
||||
+ elem.remove(e)
|
||||
+ elem.extend(iter([e]))
|
||||
+ self.serialize_check(elem, '<body><tag /><tag2 /></body>')
|
||||
+ elem.remove(e)
|
||||
+
|
||||
+ element = test_xml_etree.ET.Element("tag", key="value")
|
||||
+ self.serialize_check(element, '<tag key="value" />') # 1
|
||||
+ subelement = test_xml_etree.ET.Element("subtag")
|
||||
+ element.append(subelement)
|
||||
+ self.serialize_check(element, '<tag key="value"><subtag /></tag>') # 2
|
||||
+ element.insert(0, subelement)
|
||||
+ self.serialize_check(element,
|
||||
+ '<tag key="value"><subtag /><subtag /></tag>') # 3
|
||||
+ element.remove(subelement)
|
||||
+ self.serialize_check(element, '<tag key="value"><subtag /></tag>') # 4
|
||||
+ element.remove(subelement)
|
||||
+ self.serialize_check(element, '<tag key="value" />') # 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, '<tag key="value" />') # 6
|
||||
+ element[0:0] = [subelement, subelement, subelement]
|
||||
+ self.serialize_check(element[1], '<subtag />')
|
||||
+ 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,
|
||||
+ '<tag key="value"><subtag /><subtag /></tag>')
|
||||
+
|
||||
+ @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: ...
|
||||
+ <changes>
|
||||
+ # 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 <daniel.hillier@gmail.com>
|
||||
# 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 <daniel.hillier@gmail.com>
|
||||
# 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",
|
||||
)
|
||||
Reference in New Issue
Block a user