Move exception formatting utilities to `sphinx.util.exceptions`

This commit is contained in:
Adam Turner
2022-09-09 20:57:18 +01:00
parent 081d72b2f7
commit 5eb79c126a
4 changed files with 89 additions and 72 deletions

View File

@@ -22,6 +22,16 @@ The following is a list of deprecated interfaces.
- Removed
- Alternatives
* - ``sphinx.util.save_traceback``
- 6.1
- 8.0
- ``sphinx.util.exceptions.save_traceback``
* - ``sphinx.util.format_exception_cut_frames``
- 6.1
- 8.0
- ``sphinx.util.exceptions.format_exception_cut_frames``
* - ``sphinx.util.epoch_to_rfc1123``
- 6.1
- 8.0

View File

@@ -20,9 +20,10 @@ from sphinx import __display_version__, package_dir
from sphinx.application import Sphinx
from sphinx.errors import SphinxError
from sphinx.locale import __
from sphinx.util import Tee, format_exception_cut_frames, save_traceback
from sphinx.util import Tee
from sphinx.util.console import color_terminal, nocolor, red, terminal_safe # type: ignore
from sphinx.util.docutils import docutils_namespace, patch_docutils
from sphinx.util.exceptions import format_exception_cut_frames, save_traceback
from sphinx.util.osutil import abspath, ensuredir
@@ -53,7 +54,7 @@ def handle_exception(
elif isinstance(exception, UnicodeError):
print(red(__('Encoding error:')), file=stderr)
print(terminal_safe(str(exception)), file=stderr)
tbpath = save_traceback(app)
tbpath = save_traceback(app, exception)
print(red(__('The full traceback has been saved in %s, if you want '
'to report the issue to the developers.') % tbpath),
file=stderr)
@@ -68,7 +69,7 @@ def handle_exception(
else:
print(red(__('Exception occurred:')), file=stderr)
print(format_exception_cut_frames().rstrip(), file=stderr)
tbpath = save_traceback(app)
tbpath = save_traceback(app, exception)
print(red(__('The full traceback has been saved in %s, if you '
'want to report the issue to the developers.') % tbpath),
file=stderr)

View File

@@ -6,23 +6,21 @@ import hashlib
import os
import posixpath
import re
import sys
import tempfile
import traceback
import warnings
from importlib import import_module
from os import path
from typing import IO, TYPE_CHECKING, Any, Iterable
from typing import IO, Any, Iterable
from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit
from sphinx.deprecation import RemovedInSphinx70Warning, deprecated_alias
from sphinx.errors import ExtensionError, FiletypeNotFoundError, SphinxParallelError
from sphinx.errors import ExtensionError, FiletypeNotFoundError
from sphinx.locale import __
from sphinx.util import display as _display
from sphinx.util import exceptions as _exceptions
from sphinx.util import http_date as _http_date
from sphinx.util import logging
from sphinx.util import osutil as _osutil
from sphinx.util.console import strip_colors
from sphinx.util.console import strip_colors # NoQA: F401
from sphinx.util.matching import patfilter # noqa: F401
from sphinx.util.nodes import (caption_ref_re, explicit_title_re, # noqa: F401
nested_parse_with_titles, split_explicit_title)
@@ -32,10 +30,6 @@ from sphinx.util.osutil import (SEP, copyfile, copytimes, ensuredir, # noqa: F4
make_filename, mtimes_of_files, os_path, relative_uri)
from sphinx.util.typing import PathMatcher
if TYPE_CHECKING:
from sphinx.application import Sphinx
logger = logging.getLogger(__name__)
# Generally useful regular expressions.
@@ -193,54 +187,6 @@ class DownloadFiles(dict):
self.add_file(docname, filename)
_DEBUG_HEADER = '''\
# Sphinx version: %s
# Python version: %s (%s)
# Docutils version: %s %s
# Jinja2 version: %s
# Last messages:
%s
# Loaded extensions:
'''
def save_traceback(app: Sphinx | None) -> str:
"""Save the current exception's traceback in a temporary file."""
import platform
import docutils
import jinja2
import sphinx
exc = sys.exc_info()[1]
if isinstance(exc, SphinxParallelError):
exc_format = '(Error in parallel process)\n' + exc.traceback
else:
exc_format = traceback.format_exc()
fd, path = tempfile.mkstemp('.log', 'sphinx-err-')
last_msgs = ''
if app is not None:
last_msgs = '\n'.join(
'# %s' % strip_colors(s).strip()
for s in app.messagelog)
os.write(fd, (_DEBUG_HEADER %
(sphinx.__display_version__,
platform.python_version(),
platform.python_implementation(),
docutils.__version__, docutils.__version_details__,
jinja2.__version__,
last_msgs)).encode())
if app is not None:
for ext in app.extensions.values():
modfile = getattr(ext.module, '__file__', 'unknown')
if ext.version != 'builtin':
os.write(fd, ('# %s (%s) from %s\n' %
(ext.name, ext.version, modfile)).encode())
os.write(fd, exc_format.encode())
os.close(fd)
return path
def get_full_modname(modname: str, attribute: str) -> str | None:
if modname is None:
# Prevents a TypeError: if the last getattr() call will return None
@@ -359,17 +305,6 @@ def split_index_msg(type: str, value: str) -> list[str]:
return result
def format_exception_cut_frames(x: int = 1) -> str:
"""Format an exception with traceback, but only the last x frames."""
typ, val, tb = sys.exc_info()
# res = ['Traceback (most recent call last):\n']
res: list[str] = []
tbres = traceback.format_tb(tb)
res += tbres[-x:]
res += traceback.format_exception_only(typ, val)
return ''.join(res)
def import_object(objname: str, source: str | None = None) -> Any:
"""Import python object by qualname."""
try:
@@ -473,6 +408,8 @@ deprecated_alias('sphinx.util',
'progress_message': _display.progress_message,
'epoch_to_rfc1123': _http_date.epoch_to_rfc1123,
'rfc1123_to_epoch': _http_date.rfc1123_to_epoch,
'save_traceback': _exceptions.save_traceback,
'format_exception_cut_frames': _exceptions.format_exception_cut_frames,
},
RemovedInSphinx70Warning,
{
@@ -483,4 +420,6 @@ deprecated_alias('sphinx.util',
'progress_message': 'sphinx.util.display.progress_message',
'epoch_to_rfc1123': 'sphinx.http_date.epoch_to_rfc1123',
'rfc1123_to_epoch': 'sphinx.http_date.rfc1123_to_epoch',
'save_traceback': 'sphinx.exceptions.save_traceback',
'format_exception_cut_frames': 'sphinx.exceptions.format_exception_cut_frames', # NoQA: E501
})

67
sphinx/util/exceptions.py Normal file
View File

@@ -0,0 +1,67 @@
from __future__ import annotations
import sys
import traceback
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING
from sphinx.errors import SphinxParallelError
from sphinx.util.console import strip_colors
if TYPE_CHECKING:
from sphinx.application import Sphinx
def save_traceback(app: Sphinx | None, exc: BaseException) -> str:
"""Save the given exception's traceback in a temporary file."""
import platform
import docutils
import jinja2
import pygments
import sphinx
if isinstance(exc, SphinxParallelError):
exc_format = '(Error in parallel process)\n' + exc.traceback
else:
exc_format = traceback.format_exc()
if app is None:
last_msgs = exts_list = ''
else:
extensions = app.extensions.values()
last_msgs = '\n'.join(f'# {strip_colors(s).strip()}' for s in app.messagelog)
exts_list = '\n'.join(f'# {ext.name} ({ext.version})' for ext in extensions
if ext.version != 'builtin')
with NamedTemporaryFile('w', suffix='.log', prefix='sphinx-err-', delete=False) as f:
f.write(f"""\
# Platform: {sys.platform}; ({platform.platform()})
# Sphinx version: {sphinx.__display_version__}
# Python version: {platform.python_version()} ({platform.python_implementation()})
# Docutils version: {docutils.__version__}
# Jinja2 version: {jinja2.__version__}
# Pygments version: {pygments.__version__}
# Last messages:
{last_msgs}
# Loaded extensions:
{exts_list}
# Traceback:
{exc_format}
""")
return f.name
def format_exception_cut_frames(x: int = 1) -> str:
"""Format an exception with traceback, but only the last x frames."""
typ, val, tb = sys.exc_info()
# res = ['Traceback (most recent call last):\n']
res: list[str] = []
tbres = traceback.format_tb(tb)
res += tbres[-x:]
res += traceback.format_exception_only(typ, val)
return ''.join(res)