Repository: zopefoundation/zope.exceptions (language: python)

Issue:
Fix test_format_exception_as_html on python 3.13
The Fedora project has rebuilt all of its python packages with python 3.13.0b3.  This fixes the test failure reported in https://github.com/zopefoundation/zope.exceptions/issues/34.
test_format_exception_as_html fails with Python 3.13
## BUG/PROBLEM REPORT / FEATURE REQUEST

### What I did:

Run tests with Python 3.13.0b2 (`tox -e py313`).

### What I expect to happen:

Tests passing.

### What actually happened:

```pytb
Failure in test test_format_exception_as_html (zope.exceptions.tests.test_exceptionformatter.Test_format_exception.test_format_exception_as_html)
Traceback (most recent call last):
  File "/usr/lib/python3.13/unittest/case.py", line 58, in testPartExecutor
    yield
  File "/usr/lib/python3.13/unittest/case.py", line 651, in run
    self._callTestMethod(testMethod)
  File "/usr/lib/python3.13/unittest/case.py", line 606, in _callTestMethod
    if method() is not None:
  File "/tmp/zope.exceptions/src/zope/exceptions/tests/test_exceptionformatter.py", line 779, in test_format_exception_as_html
    self.assertEqual(expected, result)
  File "/usr/lib/python3.13/unittest/case.py", line 907, in assertEqual
    assertion_func(first, second, msg=msg)
  File "/usr/lib/python3.13/unittest/case.py", line 1273, in assertMultiLineEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "/usr/lib/python3.13/unittest/case.py", line 732, in fail
    raise self.failureException(msg)
AssertionError: '<p>T[272 chars]Error: invalid syntax<br />\n</p>' != '<p>T[272 chars]Error: Expected one or more names after \'import\'<br />\n</p>'
  <p>Traceback (most recent call last):</p>
  <ul>
  <li>  Module zope.exceptions.tests.test_exceptionformatter, line ABC, in test_format_exception_as_html<br />
      exec('import')</li>
  </ul><p>  File "&lt;string&gt;", line 1<br />
      import<br />
            ^<br />
- SyntaxError: invalid syntax<br />
+ SyntaxError: Expected one or more names after 'import'<br />
  </p>
```

Apparently it's just a matter of improved exception message.

### What version of Python and Zope/Addons I am using:

Gentoo Linux amd64
Python 3.13.0b2
zope.exceptions 69ee8af262c40432e237202dc20fc3ae5d5ee5a6


Discussion / hints:

@mgorny Thank you for reporting. I'll put it onto the list of issues needed to solve to support Python 3.13.

Fix patch:
diff --git a/.meta.toml b/.meta.toml
index f819477..43e43c6 100644
--- a/.meta.toml
+++ b/.meta.toml
@@ -2,14 +2,14 @@
 # https://github.com/zopefoundation/meta/tree/master/config/pure-python
 [meta]
 template = "pure-python"
-commit-id = "a6fb3886"
+commit-id = "994c74d7"
 
 [python]
 with-pypy = true
 with-docs = true
 with-sphinx-doctests = true
 with-windows = false
-with-future-python = false
+with-future-python = true
 with-macos = false
 
 [tox]
diff --git a/CHANGES.rst b/CHANGES.rst
index 490937b..63c7617 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,8 @@
 
 - Drop support for Python 3.7.
 
+- Add preliminary support for Python 3.13 as of 3.13.rc1
+
 
 5.1 (2024-06-07)
 ================
diff --git a/tox.ini b/tox.ini
index 46baa20..d2b7639 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,6 +10,7 @@ envlist =
     py310
     py311
     py312
+    py313
     pypy3
     docs
     coverage
@@ -18,8 +19,10 @@ envlist =
 usedevelop = true
 package = wheel
 wheel_build_env = .pkg
+pip_pre = py313: true
 deps =
     setuptools < 69
+    Sphinx
 setenv =
     py312: VIRTUALENV_PIP=23.1.2
     py312: PIP_REQUIRE_VIRTUALENV=0
@@ -80,6 +83,7 @@ allowlist_externals =
     mkdir
 deps =
     coverage
+    Sphinx
 commands =
     mkdir -p {toxinidir}/parts/htmlcov
     coverage run -m zope.testrunner --test-path=src {posargs:-vc}


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 4e73ac1..ef4529b 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -27,6 +27,7 @@ jobs:
         - ["3.10",  "py310"]
         - ["3.11",  "py311"]
         - ["3.12",  "py312"]
+        - ["3.13",  "py313"]
         - ["pypy-3.10", "pypy3"]
         - ["3.11",   "docs"]
         - ["3.11",   "coverage"]
@@ -40,6 +41,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/zope/exceptions/tests/test_exceptionformatter.py b/src/zope/exceptions/tests/test_exceptionformatter.py
index 92ea79a..7b84261 100644
--- a/src/zope/exceptions/tests/test_exceptionformatter.py
+++ b/src/zope/exceptions/tests/test_exceptionformatter.py
@@ -23,7 +23,8 @@
 from urllib.error import HTTPError
 
 
-IS_PY39_OR_GREATER = sys.version_info >= (3, 9)
+IS_PY38_OR_OLDER = sys.version_info < (3, 9)
+IS_PY312_OR_OLDER = sys.version_info < (3, 13)
 
 
 class TextExceptionFormatterTests(unittest.TestCase):
@@ -762,16 +763,20 @@ def test_format_exception_as_html(self):
                 exec('import')</li>
             </ul><p>  File "&lt;string&gt;", line 1<br />
                 import<br />
-                     ^<br />
-            SyntaxError: invalid syntax<br />
+                      ^<br />
+            SyntaxError: Expected one or more names after 'import'<br />
             </p>""").format(
-                module='zope.exceptions.tests.test_exceptionformatter',
-                fn='test_format_exception_as_html',
+            module='zope.exceptions.tests.test_exceptionformatter',
+            fn='test_format_exception_as_html',
         )
-        if IS_PY39_OR_GREATER:  # pragma: no cover
-            # Python 3.9 moves the pointer after the statement instead to the
-            # last character of it:
-            expected = expected.replace('^<br />', ' ^<br />')
+        if IS_PY38_OR_OLDER:  # pragma: no cover
+            # Python <= 3.8 hand the pointer at the last character of the
+            # statement:
+            expected = expected.replace(' ^<br />', '^<br />')
+        if IS_PY312_OR_OLDER:  # pragma: no cover
+            # Python <= 3.12 had a different error message:
+            expected = expected.replace(
+                "Expected one or more names after 'import'", "invalid syntax")
         # HTML formatter uses Windows line endings for some reason.
         result = result.replace('\r\n', '\n')
         result = re.sub(r'line \d\d\d,', 'line ABC,', result)