summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Howitz <mh@gocept.com>2022-11-10 08:27:35 +0100
committerGitHub <noreply@github.com>2022-11-10 08:27:35 +0100
commite67ea56a759ef316501d9187a7138f606330826b (patch)
treea5f02ee9ab8c65f0bd8ac64aba1b2c4aaa91ad6f
parent9b1b180deab42116d7b8f9ea9f73ecc180b639e9 (diff)
parent51ed722fb9cc42b520481cd5046192a9fab7b31c (diff)
downloadzope-exceptions-e67ea56a759ef316501d9187a7138f606330826b.tar.gz
Merge pull request #26 from zopefoundation/maurits-python311
Support Python 3.11
-rw-r--r--.github/workflows/tests.yml6
-rw-r--r--.gitignore1
-rw-r--r--.meta.toml2
-rw-r--r--CHANGES.rst6
-rw-r--r--setup.cfg11
-rw-r--r--setup.py5
-rw-r--r--src/zope/exceptions/__init__.py16
-rw-r--r--src/zope/exceptions/exceptionformatter.py14
-rw-r--r--src/zope/exceptions/interfaces.py2
-rw-r--r--src/zope/exceptions/log.py3
-rw-r--r--src/zope/exceptions/tests/test_exceptionformatter.py59
-rw-r--r--tox.ini19
12 files changed, 121 insertions, 23 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d31f648..932104c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,12 +28,14 @@ jobs:
- ["3.8", "py38"]
- ["3.9", "py39"]
- ["3.10", "py310"]
- - ["pypy2", "pypy"]
- - ["pypy3", "pypy3"]
+ - ["3.11", "py311"]
+ - ["pypy-2.7", "pypy"]
+ - ["pypy-3.7", "pypy3"]
- ["3.9", "docs"]
- ["3.9", "coverage"]
runs-on: ${{ matrix.os }}-latest
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: ${{ matrix.config[1] }}
steps:
- uses: actions/checkout@v2
diff --git a/.gitignore b/.gitignore
index c724a76..1f321f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,4 +28,5 @@ lib64
log/
parts/
pyvenv.cfg
+testing.log
var/
diff --git a/.meta.toml b/.meta.toml
index aa2e6d7..bacd0f1 100644
--- a/.meta.toml
+++ b/.meta.toml
@@ -2,7 +2,7 @@
# https://github.com/zopefoundation/meta/tree/master/config/pure-python
[meta]
template = "pure-python"
-commit-id = "121e74bd9c9718abd9a1a079e6ede252c1a0ba7d"
+commit-id = "b4dd6f9ffd3d6a2cde7dc70512c62d4c7ed22cd6"
[python]
with-pypy = true
diff --git a/CHANGES.rst b/CHANGES.rst
index 303929d..5d3521b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,7 +5,11 @@
4.6 (unreleased)
================
-- Nothing changed yet.
+- Catch exceptions in ``formatExceptionOnly``.
+ Getting an exception when reporting about a different exception is not helpful.
+ On Python 3.11 this is needed for some HTTPErrors.
+
+- Add official support for Python 3.11.
4.5 (2022-02-11)
diff --git a/setup.cfg b/setup.cfg
index 8b04203..124476d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -12,3 +12,14 @@ ignore =
.meta.toml
docs/_build/html/_sources/*
docs/_build/doctest/*
+
+[isort]
+force_single_line = True
+combine_as_imports = True
+sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER
+known_third_party = six, docutils, pkg_resources
+known_zope =
+known_first_party =
+default_section = ZOPE
+line_length = 79
+lines_after_imports = 2
diff --git a/setup.py b/setup.py
index 05fdec7..78bd8a6 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,9 @@
"""Setup for zope.exceptions package
"""
import os
-from setuptools import setup, find_packages
+
+from setuptools import find_packages
+from setuptools import setup
def read(*rnames):
@@ -50,6 +52,7 @@ setup(
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Natural Language :: English',
diff --git a/src/zope/exceptions/__init__.py b/src/zope/exceptions/__init__.py
index 84d1dd2..3dcdf02 100644
--- a/src/zope/exceptions/__init__.py
+++ b/src/zope/exceptions/__init__.py
@@ -17,14 +17,14 @@ These exceptions are so general purpose that they don't belong in Zope
application-specific packages.
"""
+from zope.exceptions.exceptionformatter import extract_stack
+from zope.exceptions.exceptionformatter import format_exception
+from zope.exceptions.exceptionformatter import print_exception
from zope.exceptions.interfaces import DuplicationError
from zope.exceptions.interfaces import IDuplicationError
-from zope.exceptions.interfaces import UserError
from zope.exceptions.interfaces import IUserError
+from zope.exceptions.interfaces import UserError
-from zope.exceptions.exceptionformatter import format_exception
-from zope.exceptions.exceptionformatter import print_exception
-from zope.exceptions.exceptionformatter import extract_stack
__all__ = [
'DuplicationError', 'IDuplicationError', 'UserError', 'IUserError',
@@ -40,12 +40,12 @@ except ImportError as v: # pragma: no cover
if 'security' not in str(v):
raise
else: # pragma: no cover
- from zope.security.interfaces import IUnauthorized
- from zope.security.interfaces import Unauthorized
- from zope.security.interfaces import IForbidden
- from zope.security.interfaces import IForbiddenAttribute
from zope.security.interfaces import Forbidden
from zope.security.interfaces import ForbiddenAttribute
+ from zope.security.interfaces import IForbidden
+ from zope.security.interfaces import IForbiddenAttribute
+ from zope.security.interfaces import IUnauthorized
+ from zope.security.interfaces import Unauthorized
__all__ += [
'IUnauthorized', 'Unauthorized', 'IForbidden', 'IForbiddenAttribute',
'Forbidden', 'ForbiddenAttribute',
diff --git a/src/zope/exceptions/exceptionformatter.py b/src/zope/exceptions/exceptionformatter.py
index d74dcf4..296f4ba 100644
--- a/src/zope/exceptions/exceptionformatter.py
+++ b/src/zope/exceptions/exceptionformatter.py
@@ -15,13 +15,17 @@
optionally in HTML.
"""
import sys
+
+
try:
from html import escape
except ImportError: # pragma: PY2
from cgi import escape
+
import linecache
import traceback
+
DEBUG_EXCEPTION_FORMATTER = 1
@@ -167,7 +171,15 @@ class TextExceptionFormatter(object):
return self.line_sep.join(result)
def formatExceptionOnly(self, etype, value):
- result = ''.join(traceback.format_exception_only(etype, value))
+ # We don't want to get an error when we format an error, so we
+ # compensate in our code. For example, on Python 3.11.0 HTTPError
+ # gives an unhelpful KeyError in tempfile when Python formats it.
+ # See https://github.com/python/cpython/issues/90113
+ try:
+ result = ''.join(traceback.format_exception_only(etype, value))
+ except Exception: # pragma: no cover
+ # This code branch is only covered on Python 3.11.
+ result = str(value)
return result
def formatLastLine(self, exc_line):
diff --git a/src/zope/exceptions/interfaces.py b/src/zope/exceptions/interfaces.py
index 4f792a5..525e4b0 100644
--- a/src/zope/exceptions/interfaces.py
+++ b/src/zope/exceptions/interfaces.py
@@ -28,8 +28,8 @@ arguments to pass to the factory. The traceback formatter makes an
effort to clearly present the information provided by the
ITracebackSupplement.
"""
-from zope.interface import Interface
from zope.interface import Attribute
+from zope.interface import Interface
from zope.interface import implementer
diff --git a/src/zope/exceptions/log.py b/src/zope/exceptions/log.py
index d5a6c30..82049aa 100644
--- a/src/zope/exceptions/log.py
+++ b/src/zope/exceptions/log.py
@@ -14,11 +14,12 @@
"""Log formatter that enhances tracebacks with extra information.
"""
-import logging
import io
+import logging
from zope.exceptions.exceptionformatter import print_exception
+
Buffer = io.StringIO if bytes is not str else io.BytesIO # PY2
diff --git a/src/zope/exceptions/tests/test_exceptionformatter.py b/src/zope/exceptions/tests/test_exceptionformatter.py
index cf4ea9b..cedfc5d 100644
--- a/src/zope/exceptions/tests/test_exceptionformatter.py
+++ b/src/zope/exceptions/tests/test_exceptionformatter.py
@@ -13,8 +13,15 @@
##############################################################################
"""ExceptionFormatter tests.
"""
-import unittest
import sys
+import unittest
+
+
+try:
+ from urllib.error import HTTPError
+except ImportError:
+ # BBB for Python 2.7
+ from urllib2 import HTTPError
IS_PY39_OR_GREATER = sys.version_info >= (3, 9)
@@ -278,6 +285,37 @@ class TextExceptionFormatterTests(unittest.TestCase):
''.join(
traceback.format_exception_only(ValueError, err)))
+ def test_formatExceptionOnly_httperror(self):
+ # On Python 3.11.0 HTTPError may behave wrongly, giving a KeyError in
+ # tempfile when Python tries to format it.
+ # See https://github.com/python/cpython/issues/90113
+ # or examples in Plone tests, especially doctests:
+ # https://github.com/plone/Products.CMFPlone/issues/3663
+ # We don't want to get an error when we format an error,
+ # so let's compensate in our code.
+ fmt = self._makeOne()
+ err = HTTPError('url', 400, 'oops', [], None)
+ result = fmt.formatExceptionOnly(HTTPError, err).strip()
+ # The output can differ too much per Python version,
+ # but it is just one line when stripped.
+ self.assertIn("400", result)
+ self.assertIn("oops", result)
+ self.assertIn("Error", result)
+ self.assertEqual(len(result.splitlines()), 1)
+
+ def test_formatException_httperror(self):
+ # See test_formatExceptionOnly_httperror.
+ # Here we check that formatException works.
+ fmt = self._makeOne()
+ err = HTTPError('url', 400, 'oops', [], None)
+ result = fmt.formatException(HTTPError, err, None)
+ self.assertEqual(result[0], 'Traceback (most recent call last):\n')
+ last = result[-1]
+ # The output can differ per Python version.
+ self.assertIn("400", last)
+ self.assertIn("oops", last)
+ self.assertIn("Error", last)
+
def test_formatLastLine(self):
fmt = self._makeOne()
self.assertEqual(fmt.formatLastLine('XXX'), 'XXX')
@@ -709,9 +747,10 @@ class Test_format_exception(unittest.TestCase):
def test_format_exception_as_html(self):
# Test for format_exception (as_html=True)
- from zope.exceptions.exceptionformatter import format_exception
- from textwrap import dedent
import re
+ from textwrap import dedent
+
+ from zope.exceptions.exceptionformatter import format_exception
try:
exec('import')
except SyntaxError:
@@ -885,8 +924,11 @@ class DummySupplement(object):
class DummyTB(object):
+ # https://docs.python.org/3/reference/datamodel.html#traceback-objects
+ tb_frame = None
tb_lineno = 14
tb_next = None
+ tb_lasti = 1
class DummyFrame(object):
@@ -903,6 +945,17 @@ class DummyCode(object):
co_filename = 'dummy/filename.py'
co_name = 'dummy_function'
+ def co_positions(self):
+ # New in Python 3.11.
+ # https://docs.python.org/3/reference/datamodel.html#codeobject.co_positions
+ # Note that this is not called for DummyTB if you have tb_lasti=-1.
+ # The 27 in the return value is chosen to match tb_recurse.tb_lineno=27
+ # in test_formatException_recursion_in_tb_stack in this file.
+ # The rest is random.
+ # Note that this code is only called on Python 3.11+, so we mark it for
+ # the coverage tool.
+ return [(27, 2, 3, 4)] # pragma: no cover
+
class _Monkey(object):
# context-manager for replacing module names in the scope of a test.
diff --git a/tox.ini b/tox.ini
index 1e36bad..a29d4ba 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,7 @@ envlist =
py38
py39
py310
+ py311
pypy
pypy3
docs
@@ -29,15 +30,25 @@ extras =
[testenv:lint]
basepython = python3
skip_install = true
+commands =
+ isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py
+ flake8 src setup.py
+ check-manifest
+ check-python-versions
deps =
- flake8
check-manifest
check-python-versions >= 0.19.1
wheel
+ flake8
+ isort
+
+[testenv:isort-apply]
+basepython = python3
+commands_pre =
+deps =
+ isort
commands =
- flake8 src setup.py
- check-manifest
- check-python-versions
+ isort {toxinidir}/src {toxinidir}/setup.py []
[testenv:docs]
basepython = python3