mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Consolidate both `handle_exception()
` implementations
This commit is contained in:
parent
dec45eaf28
commit
404e4ffbed
@ -2,15 +2,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
from typing import TYPE_CHECKING
|
||||||
from typing import TYPE_CHECKING, TextIO
|
|
||||||
|
|
||||||
from sphinx.errors import SphinxParallelError
|
from sphinx.errors import SphinxError, SphinxParallelError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Final
|
from collections.abc import Collection
|
||||||
|
from typing import Final, Protocol
|
||||||
|
|
||||||
from sphinx.application import Sphinx
|
from sphinx.extension import Extension
|
||||||
|
|
||||||
|
class SupportsWrite(Protocol):
|
||||||
|
def write(self, text: str, /) -> int | None: ...
|
||||||
|
|
||||||
|
|
||||||
_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
|
_CSI: Final[str] = re.escape('\x1b[') # 'ESC [': Control Sequence Introducer
|
||||||
@ -50,6 +53,34 @@ def strip_escape_sequences(text: str, /) -> str:
|
|||||||
return _ANSI_CODES.sub('', text)
|
return _ANSI_CODES.sub('', text)
|
||||||
|
|
||||||
|
|
||||||
|
def full_exception_context(
|
||||||
|
exception: BaseException,
|
||||||
|
*,
|
||||||
|
message_log: Collection[str] = (),
|
||||||
|
extensions: Collection[Extension] = (),
|
||||||
|
) -> str:
|
||||||
|
"""Return a formatted message containing useful debugging context."""
|
||||||
|
last_msgs = '\n'.join(f'* {strip_escape_sequences(s)}' for s in message_log)
|
||||||
|
exts_list = '\n'.join(
|
||||||
|
f'* {ext.name} ({ext.version})'
|
||||||
|
for ext in extensions
|
||||||
|
if ext.version != 'builtin'
|
||||||
|
)
|
||||||
|
exc_format = format_traceback(exception)
|
||||||
|
return error_info(last_msgs or 'None.', exts_list or 'None.', exc_format)
|
||||||
|
|
||||||
|
|
||||||
|
def format_traceback(exception: BaseException, /) -> str:
|
||||||
|
"""Format the given exception's traceback."""
|
||||||
|
if isinstance(exception, SphinxParallelError):
|
||||||
|
return f'(Error in parallel process)\n{exception.traceback}'
|
||||||
|
else:
|
||||||
|
from traceback import format_exception
|
||||||
|
|
||||||
|
exc_format = ''.join(format_exception(exception))
|
||||||
|
return exc_format
|
||||||
|
|
||||||
|
|
||||||
def error_info(messages: str, extensions: str, traceback: str) -> str:
|
def error_info(messages: str, extensions: str, traceback: str) -> str:
|
||||||
"""Format the traceback and extensions list with environment information."""
|
"""Format the traceback and extensions list with environment information."""
|
||||||
import platform
|
import platform
|
||||||
@ -88,37 +119,26 @@ Traceback
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def format_traceback(app: Sphinx | None, exc: BaseException) -> str:
|
def save_traceback(
|
||||||
"""Format the given exception's traceback with environment information."""
|
exception: BaseException,
|
||||||
if isinstance(exc, SphinxParallelError):
|
*,
|
||||||
exc_format = '(Error in parallel process)\n' + exc.traceback
|
message_log: Collection[str] = (),
|
||||||
else:
|
extensions: Collection[Extension] = (),
|
||||||
import traceback
|
) -> str:
|
||||||
|
|
||||||
exc_format = traceback.format_exc()
|
|
||||||
|
|
||||||
last_msgs = exts_list = ''
|
|
||||||
if app is not None:
|
|
||||||
extensions = app.extensions.values()
|
|
||||||
last_msgs = '\n'.join(f'* {strip_escape_sequences(s)}' for s in app.messagelog)
|
|
||||||
exts_list = '\n'.join(
|
|
||||||
f'* {ext.name} ({ext.version})'
|
|
||||||
for ext in extensions
|
|
||||||
if ext.version != 'builtin'
|
|
||||||
)
|
|
||||||
|
|
||||||
return error_info(last_msgs, exts_list, exc_format)
|
|
||||||
|
|
||||||
|
|
||||||
def save_traceback(app: Sphinx | None, exc: BaseException) -> str:
|
|
||||||
"""Save the given exception's traceback in a temporary file."""
|
"""Save the given exception's traceback in a temporary file."""
|
||||||
output = format_traceback(app=app, exc=exc)
|
output = full_exception_context(
|
||||||
|
exception=exception,
|
||||||
|
message_log=message_log,
|
||||||
|
extensions=extensions,
|
||||||
|
)
|
||||||
filename = write_temporary_file(output)
|
filename = write_temporary_file(output)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
def write_temporary_file(content: str) -> str:
|
def write_temporary_file(content: str) -> str:
|
||||||
"""Write content to a temporary file and return the filename."""
|
"""Write content to a temporary file and return the filename."""
|
||||||
|
import tempfile
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
with tempfile.NamedTemporaryFile(
|
||||||
'w', encoding='utf-8', suffix='.log', prefix='sphinx-err-', delete=False
|
'w', encoding='utf-8', suffix='.log', prefix='sphinx-err-', delete=False
|
||||||
) as f:
|
) as f:
|
||||||
@ -131,18 +151,18 @@ def handle_exception(
|
|||||||
exception: BaseException,
|
exception: BaseException,
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
stderr: TextIO = sys.stderr,
|
stderr: SupportsWrite = sys.stderr,
|
||||||
use_pdb: bool = False,
|
use_pdb: bool = False,
|
||||||
print_traceback: bool = False,
|
print_traceback: bool = False,
|
||||||
app: Sphinx | None = None,
|
message_log: Collection[str] = (),
|
||||||
|
extensions: Collection[Extension] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
from bdb import BdbQuit
|
from bdb import BdbQuit
|
||||||
from traceback import TracebackException, print_exc
|
from traceback import TracebackException
|
||||||
|
|
||||||
from docutils.utils import SystemMessage
|
from docutils.utils import SystemMessage
|
||||||
|
|
||||||
from sphinx._cli.util.colour import red
|
from sphinx._cli.util.colour import red
|
||||||
from sphinx.errors import SphinxError
|
|
||||||
from sphinx.locale import __
|
from sphinx.locale import __
|
||||||
|
|
||||||
if isinstance(exception, BdbQuit):
|
if isinstance(exception, BdbQuit):
|
||||||
@ -156,14 +176,14 @@ def handle_exception(
|
|||||||
|
|
||||||
print_err()
|
print_err()
|
||||||
if print_traceback or use_pdb:
|
if print_traceback or use_pdb:
|
||||||
print_exc(file=stderr)
|
print_err(format_traceback(exception))
|
||||||
print_err()
|
print_err()
|
||||||
|
|
||||||
if use_pdb:
|
if use_pdb:
|
||||||
from pdb import post_mortem
|
from pdb import post_mortem
|
||||||
|
|
||||||
print_red(__('Exception occurred, starting debugger:'))
|
print_red(__('Exception occurred, starting debugger:'))
|
||||||
post_mortem()
|
post_mortem(exception.__traceback__)
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(exception, KeyboardInterrupt):
|
if isinstance(exception, KeyboardInterrupt):
|
||||||
@ -193,7 +213,7 @@ def handle_exception(
|
|||||||
__(
|
__(
|
||||||
'This can happen with very large or deeply nested source '
|
'This can happen with very large or deeply nested source '
|
||||||
'files. You can carefully increase the default Python '
|
'files. You can carefully increase the default Python '
|
||||||
'recursion limit of 1000 in conf.py with e.g.:'
|
'recursion limit of 1,000 in conf.py with e.g.:'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print_err('\n import sys\n sys.setrecursionlimit(1_500)\n')
|
print_err('\n import sys\n sys.setrecursionlimit(1_500)\n')
|
||||||
@ -205,7 +225,9 @@ def handle_exception(
|
|||||||
|
|
||||||
print_red(__('Exception occurred:'))
|
print_red(__('Exception occurred:'))
|
||||||
print_err(formatted_tb)
|
print_err(formatted_tb)
|
||||||
traceback_info_path = save_traceback(app, exception)
|
traceback_info_path = save_traceback(
|
||||||
|
exception, message_log=message_log, extensions=extensions
|
||||||
|
)
|
||||||
print_err(__('The full traceback has been saved in:'))
|
print_err(__('The full traceback has been saved in:'))
|
||||||
print_err(traceback_info_path)
|
print_err(traceback_info_path)
|
||||||
print_err()
|
print_err()
|
||||||
|
@ -3,36 +3,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import bdb
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import locale
|
import locale
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import pdb # NoQA: T100
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, TextIO
|
from typing import TYPE_CHECKING, Any, TextIO
|
||||||
|
|
||||||
from docutils.utils import SystemMessage
|
import sphinx._cli.util.errors
|
||||||
|
|
||||||
import sphinx.locale
|
import sphinx.locale
|
||||||
from sphinx import __display_version__
|
from sphinx import __display_version__
|
||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
from sphinx.errors import SphinxError, SphinxParallelError
|
|
||||||
from sphinx.locale import __
|
from sphinx.locale import __
|
||||||
from sphinx.util._io import TeeStripANSI
|
from sphinx.util._io import TeeStripANSI
|
||||||
from sphinx.util._pathlib import _StrPath
|
from sphinx.util._pathlib import _StrPath
|
||||||
from sphinx.util.console import color_terminal, nocolor, red, terminal_safe
|
from sphinx.util.console import color_terminal, nocolor
|
||||||
from sphinx.util.docutils import docutils_namespace, patch_docutils
|
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 ensuredir
|
from sphinx.util.osutil import ensuredir
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Sequence
|
from collections.abc import Collection, Sequence
|
||||||
from typing import Protocol
|
|
||||||
|
|
||||||
class SupportsWrite(Protocol):
|
from sphinx.extension import Extension
|
||||||
def write(self, text: str, /) -> int | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def handle_exception(
|
def handle_exception(
|
||||||
@ -41,92 +33,19 @@ def handle_exception(
|
|||||||
exception: BaseException,
|
exception: BaseException,
|
||||||
stderr: TextIO = sys.stderr,
|
stderr: TextIO = sys.stderr,
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(exception, bdb.BdbQuit):
|
if app is not None:
|
||||||
return
|
message_log: Sequence[str] = app.messagelog
|
||||||
|
extensions: Collection[Extension] = app.extensions.values()
|
||||||
if args.pdb:
|
|
||||||
print(
|
|
||||||
red(__('Exception occurred while building, starting debugger:')),
|
|
||||||
file=stderr,
|
|
||||||
)
|
|
||||||
traceback.print_exc()
|
|
||||||
pdb.post_mortem(sys.exc_info()[2])
|
|
||||||
else:
|
else:
|
||||||
print(file=stderr)
|
message_log = extensions = ()
|
||||||
if args.verbosity or args.traceback:
|
return sphinx._cli.util.errors.handle_exception(
|
||||||
exc = sys.exc_info()[1]
|
exception,
|
||||||
if isinstance(exc, SphinxParallelError):
|
stderr=stderr,
|
||||||
exc_format = '(Error in parallel process)\n' + exc.traceback
|
use_pdb=args.pdb,
|
||||||
print(exc_format, file=stderr)
|
print_traceback=args.verbosity or args.traceback,
|
||||||
else:
|
message_log=message_log,
|
||||||
traceback.print_exc(None, stderr)
|
extensions=extensions,
|
||||||
print(file=stderr)
|
)
|
||||||
if isinstance(exception, KeyboardInterrupt):
|
|
||||||
print(__('Interrupted!'), file=stderr)
|
|
||||||
elif isinstance(exception, SystemMessage):
|
|
||||||
print(red(__('reST markup error:')), file=stderr)
|
|
||||||
print(terminal_safe(exception.args[0]), file=stderr)
|
|
||||||
elif isinstance(exception, SphinxError):
|
|
||||||
print(red('%s:' % exception.category), file=stderr)
|
|
||||||
print(str(exception), file=stderr)
|
|
||||||
elif isinstance(exception, UnicodeError):
|
|
||||||
print(red(__('Encoding error:')), file=stderr)
|
|
||||||
print(terminal_safe(str(exception)), file=stderr)
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
elif (
|
|
||||||
isinstance(exception, RuntimeError)
|
|
||||||
and 'recursion depth' in str(exception)
|
|
||||||
): # fmt: skip
|
|
||||||
print(red(__('Recursion error:')), file=stderr)
|
|
||||||
print(terminal_safe(str(exception)), file=stderr)
|
|
||||||
print(file=stderr)
|
|
||||||
print(
|
|
||||||
__(
|
|
||||||
'This can happen with very large or deeply nested source '
|
|
||||||
'files. You can carefully increase the default Python '
|
|
||||||
'recursion limit of 1000 in conf.py with e.g.:'
|
|
||||||
),
|
|
||||||
file=stderr,
|
|
||||||
)
|
|
||||||
print(' import sys; sys.setrecursionlimit(1500)', file=stderr)
|
|
||||||
else:
|
|
||||||
print(red(__('Exception occurred:')), file=stderr)
|
|
||||||
print(format_exception_cut_frames().rstrip(), file=stderr)
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
__(
|
|
||||||
'Please also report this if it was a user error, so '
|
|
||||||
'that a better error message can be provided next time.'
|
|
||||||
),
|
|
||||||
file=stderr,
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
__(
|
|
||||||
'A bug report can be filed in the tracker at '
|
|
||||||
'<https://github.com/sphinx-doc/sphinx/issues>. Thanks!'
|
|
||||||
),
|
|
||||||
file=stderr,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def jobs_argument(value: str) -> int:
|
def jobs_argument(value: str) -> int:
|
||||||
@ -512,7 +431,19 @@ def build_main(argv: Sequence[str]) -> int:
|
|||||||
app.build(args.force_all, args.filenames)
|
app.build(args.force_all, args.filenames)
|
||||||
return app.statuscode
|
return app.statuscode
|
||||||
except (Exception, KeyboardInterrupt) as exc:
|
except (Exception, KeyboardInterrupt) as exc:
|
||||||
handle_exception(app, args, exc, args.error)
|
if app is not None:
|
||||||
|
message_log: Sequence[str] = app.messagelog
|
||||||
|
extensions: Collection[Extension] = app.extensions.values()
|
||||||
|
else:
|
||||||
|
message_log = extensions = ()
|
||||||
|
sphinx._cli.util.errors.handle_exception(
|
||||||
|
exc,
|
||||||
|
stderr=args.error,
|
||||||
|
use_pdb=args.pdb,
|
||||||
|
print_traceback=args.verbosity or args.traceback,
|
||||||
|
message_log=message_log,
|
||||||
|
extensions=extensions,
|
||||||
|
)
|
||||||
return 2
|
return 2
|
||||||
finally:
|
finally:
|
||||||
if warnfp is not None:
|
if warnfp is not None:
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class SphinxError(Exception):
|
class SphinxError(Exception):
|
||||||
"""Base class for Sphinx errors.
|
"""Base class for Sphinx errors.
|
||||||
@ -109,7 +107,7 @@ class SphinxParallelError(SphinxError):
|
|||||||
|
|
||||||
category = 'Sphinx parallel build error'
|
category = 'Sphinx parallel build error'
|
||||||
|
|
||||||
def __init__(self, message: str, traceback: Any) -> None:
|
def __init__(self, message: str, traceback: str) -> None:
|
||||||
self.message = message
|
self.message = message
|
||||||
self.traceback = traceback
|
self.traceback = traceback
|
||||||
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from sphinx._cli.util.errors import save_traceback
|
|
||||||
|
|
||||||
__all__ = 'save_traceback', 'format_exception_cut_frames'
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
Loading…
Reference in New Issue
Block a user