Repository: zopefoundation/ZConfig (language: python)

Issue:
Test failure in test_filehandler_reopen_thread_safety on Python 3.13
## BUG/PROBLEM REPORT / FEATURE REQUEST

<!--

Please do not report security-related issues here. Report them by email to security@plone.org. The Security Team will contact the relevant maintainer if necessary.

Include tracebacks, screenshots, code of debugging sessions or code that reproduces the issue if possible.
The best reproductions are in plain Zope installations without addons or at least with minimal needed addons installed.

-->

### What I did:

<!-- Enter a reproducible description, including preconditions. -->
1. Check out current `master`, abf6f26fd27b79a23978058b274e397cca72b504
2. Run `tox -e py313`

### What I expect to happen:

All tests pass, as they do for `tox -e py312`.

### What actually happened:

```
Failure in test test_filehandler_reopen_thread_safety (ZConfig.components.logger.tests.test_logger.TestReopeningLogfiles.test_filehandler_reopen_thread_safety)
Traceback (most recent call last):
  File "/usr/lib64/python3.13/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib64/python3.13/unittest/case.py", line 651, in run
    self._callTestMethod(testMethod)
  File "/usr/lib64/python3.13/unittest/case.py", line 606, in _callTestMethod
    if method() is not None:
  File "/home/ben/src/forks/ZConfig/src/ZConfig/components/logger/tests/test_logger.py", line 714, in test_filehandler_reopen_thread_safety
    self.assertEqual(calls, ["acquire", "acquire", "release", "release"])
  File "/usr/lib64/python3.13/unittest/case.py", line 907, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/usr/lib64/python3.13/unittest/case.py", line 1113, in assertListEqual
    self.assertSequenceEqual(list1, list2, msg, seq_type=list)
  File "/usr/lib64/python3.13/unittest/case.py", line 1095, in assertSequenceEqual
    self.fail(msg)
  File "/usr/lib64/python3.13/unittest/case.py", line 732, in fail
    raise self.failureException(msg)
AssertionError: Lists differ: [] != ['acquire', 'acquire', 'release', 'release']

Second list contains 4 additional elements.
First extra element 0:
'acquire'

- []
+ ['acquire', 'acquire', 'release', 'release']

------
2024-05-11T11:12:45 INFO root An info message
------
2024-05-11T11:12:45 INFO root We see an info message
------
2024-05-11T11:12:45 INFO root Here is another info message
/home/ben/src/forks/ZConfig/src/ZConfig/tests/test_schema2html.py:106: DeprecationWarning: The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
  settings=docutils.frontend.OptionParser(
/usr/lib64/python3.13/optparse.py:1000: DeprecationWarning: The frontend.Option class will be removed in Docutils 0.21 or later.
  option = self.option_class(*args, **kwargs)
/home/ben/src/forks/ZConfig/src/ZConfig/tests/test_schema2html.py:106: DeprecationWarning: The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
  settings=docutils.frontend.OptionParser(
/usr/lib64/python3.13/optparse.py:1000: DeprecationWarning: The frontend.Option class will be removed in Docutils 0.21 or later.
  option = self.option_class(*args, **kwargs)
/home/ben/src/forks/ZConfig/src/ZConfig/tests/test_schema2html.py:106: DeprecationWarning: The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
  settings=docutils.frontend.OptionParser(
/usr/lib64/python3.13/optparse.py:1000: DeprecationWarning: The frontend.Option class will be removed in Docutils 0.21 or later.
  option = self.option_class(*args, **kwargs)
/home/ben/src/forks/ZConfig/src/ZConfig/tests/test_schema2html.py:106: DeprecationWarning: The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
  settings=docutils.frontend.OptionParser(
/usr/lib64/python3.13/optparse.py:1000: DeprecationWarning: The frontend.Option class will be removed in Docutils 0.21 or later.
  option = self.option_class(*args, **kwargs)
/home/ben/src/forks/ZConfig/src/ZConfig/tests/test_schema2html.py:106: DeprecationWarning: The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.
  settings=docutils.frontend.OptionParser(
/usr/lib64/python3.13/optparse.py:1000: DeprecationWarning: The frontend.Option class will be removed in Docutils 0.21 or later.
  option = self.option_class(*args, **kwargs)
```

### What version of Python and Zope/Addons I am using:

<!-- Enter Operating system, Python and Zope versions you are using -->

Fedora Linux 39, Python 3.13.0a6, Zope versions as installed to the virtualenv by default: zope.exceptions 5.0.1, zope.interface  6.3, zope.testrunner 6.4

-----

It wasn’t immediately obvious to me what the best way to fix this should be. I believe it may be related to https://github.com/python/cpython/issues/109461 / https://github.com/python/cpython/pull/109462 / https://github.com/python/cpython/pull/109462.


Discussion / hints:

Fix patch:
diff --git a/.meta.toml b/.meta.toml
index f5a50fb..9904c8b 100644
--- a/.meta.toml
+++ b/.meta.toml
@@ -2,12 +2,12 @@
 # https://github.com/zopefoundation/meta/tree/master/config/pure-python
 [meta]
 template = "pure-python"
-commit-id = "7713fd86"
+commit-id = "e7051a16"
 
 [python]
 with-windows = false
 with-pypy = true
-with-future-python = false
+with-future-python = true
 with-docs = true
 with-sphinx-doctests = false
 with-macos = false
diff --git a/CHANGES.rst b/CHANGES.rst
index baa0ce4..2d0c9b6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,8 @@
 4.2 (unreleased)
 ================
 
+- Add support for Python 3.13.
+
 
 4.1 (2024-05-03)
 ================
diff --git a/setup.py b/setup.py
index d5fa795..c272965 100644
--- a/setup.py
+++ b/setup.py
@@ -68,6 +68,7 @@
         'Programming Language :: Python :: 3.10',
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
+        'Programming Language :: Python :: 3.13',
         'Programming Language :: Python :: Implementation :: CPython',
         'Programming Language :: Python :: Implementation :: PyPy',
         'Operating System :: OS Independent',
diff --git a/tox.ini b/tox.ini
index 040f33c..29ffc2d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,7 @@ envlist =
     py310
     py311
     py312
+    py313
     pypy3
     docs
     coverage
@@ -19,6 +20,7 @@ envlist =
 usedevelop = true
 package = wheel
 wheel_build_env = .pkg
+pip_pre = py313: true
 deps =
 setenv =
     py312: VIRTUALENV_PIP=23.1.2


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7e2d1fb..14ebf1b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,6 +28,7 @@ jobs:
         - ["3.10",  "py310"]
         - ["3.11",  "py311"]
         - ["3.12",  "py312"]
+        - ["3.13",  "py313"]
         - ["pypy-3.10", "pypy3"]
         - ["3.9",   "docs"]
         - ["3.9",   "coverage"]
@@ -41,6 +42,7 @@ jobs:
       uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.config[0] }}
+        allow-prereleases: true
     - name: Pip cache
       uses: actions/cache@v4
       with:
diff --git a/src/ZConfig/components/logger/tests/test_logger.py b/src/ZConfig/components/logger/tests/test_logger.py
index b81bdfc..ce8b446 100644
--- a/src/ZConfig/components/logger/tests/test_logger.py
+++ b/src/ZConfig/components/logger/tests/test_logger.py
@@ -699,8 +699,19 @@ def test_filehandler_reopen_thread_safety(self):
         h = self.handler_factory(fn)
 
         calls = []
-        h.acquire = lambda: calls.append("acquire")
-        h.release = lambda: calls.append("release")
+
+        class _LockMockup:
+            def acquire(*args, **kw):
+                calls.append("acquire")
+
+            __enter__ = acquire
+
+            def release(*args, **kw):
+                calls.append("release")
+
+            __exit__ = release
+
+        h.lock = _LockMockup()
 
         h.reopen()
         self.assertEqual(calls, ["acquire", "release"])