diff options
author | Michael Howitz <mh@gocept.com> | 2022-11-10 08:27:35 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-10 08:27:35 +0100 |
commit | e67ea56a759ef316501d9187a7138f606330826b (patch) | |
tree | a5f02ee9ab8c65f0bd8ac64aba1b2c4aaa91ad6f | |
parent | 9b1b180deab42116d7b8f9ea9f73ecc180b639e9 (diff) | |
parent | 51ed722fb9cc42b520481cd5046192a9fab7b31c (diff) | |
download | zope-exceptions-e67ea56a759ef316501d9187a7138f606330826b.tar.gz |
Merge pull request #26 from zopefoundation/maurits-python311
Support Python 3.11
-rw-r--r-- | .github/workflows/tests.yml | 6 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .meta.toml | 2 | ||||
-rw-r--r-- | CHANGES.rst | 6 | ||||
-rw-r--r-- | setup.cfg | 11 | ||||
-rw-r--r-- | setup.py | 5 | ||||
-rw-r--r-- | src/zope/exceptions/__init__.py | 16 | ||||
-rw-r--r-- | src/zope/exceptions/exceptionformatter.py | 14 | ||||
-rw-r--r-- | src/zope/exceptions/interfaces.py | 2 | ||||
-rw-r--r-- | src/zope/exceptions/log.py | 3 | ||||
-rw-r--r-- | src/zope/exceptions/tests/test_exceptionformatter.py | 59 | ||||
-rw-r--r-- | tox.ini | 19 |
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 @@ -28,4 +28,5 @@ lib64 log/ parts/ pyvenv.cfg +testing.log var/ @@ -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) @@ -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 @@ -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. @@ -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 |