From 5eb79c126ab501e31c9c9457ff120c97c7cc5e3e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Fri, 9 Sep 2022 20:57:18 +0100 Subject: [PATCH] Move exception formatting utilities to ``sphinx.util.exceptions`` --- doc/extdev/deprecated.rst | 10 +++++ sphinx/cmd/build.py | 7 ++-- sphinx/util/__init__.py | 77 ++++----------------------------------- sphinx/util/exceptions.py | 67 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 72 deletions(-) create mode 100644 sphinx/util/exceptions.py diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 78eda5111..8d4667beb 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -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 diff --git a/sphinx/cmd/build.py b/sphinx/cmd/build.py index a6c58a539..9fc061718 100644 --- a/sphinx/cmd/build.py +++ b/sphinx/cmd/build.py @@ -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) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 2c1cef3f6..837e41af8 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -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 }) diff --git a/sphinx/util/exceptions.py b/sphinx/util/exceptions.py new file mode 100644 index 000000000..9e2569533 --- /dev/null +++ b/sphinx/util/exceptions.py @@ -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)