Skip to content

Commit 7e4ef9f

Browse files
committed
Add _cli.util.errors
1 parent 9c0a621 commit 7e4ef9f

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed

sphinx/_cli/util/errors.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
import tempfile
6+
from typing import TYPE_CHECKING, TextIO
7+
8+
from sphinx.errors import SphinxParallelError
9+
10+
if TYPE_CHECKING:
11+
from sphinx.application import Sphinx
12+
13+
_ANSI_CODES: re.Pattern[str] = re.compile('\x1b.*?m')
14+
15+
16+
def terminal_safe(s: str, /) -> str:
17+
"""Safely encode a string for printing to the terminal."""
18+
return s.encode('ascii', 'backslashreplace').decode('ascii')
19+
20+
21+
def strip_colors(s: str, /) -> str:
22+
return _ANSI_CODES.sub('', s).strip()
23+
24+
25+
def error_info(messages: str, extensions: str, traceback: str) -> str:
26+
import platform
27+
28+
import docutils
29+
import jinja2
30+
import pygments
31+
32+
import sphinx
33+
34+
return f"""\
35+
Versions
36+
========
37+
38+
* Platform: {sys.platform}; ({platform.platform()})
39+
* Python version: {platform.python_version()} ({platform.python_implementation()})
40+
* Sphinx version: {sphinx.__display_version__}
41+
* Docutils version: {docutils.__version__}
42+
* Jinja2 version: {jinja2.__version__}
43+
* Pygments version: {pygments.__version__}
44+
45+
Last Messages
46+
=============
47+
48+
{messages}
49+
50+
Loaded Extensions
51+
=================
52+
53+
{extensions}
54+
55+
Traceback
56+
=========
57+
58+
{traceback}
59+
"""
60+
61+
62+
def save_traceback(app: Sphinx | None, exc: BaseException) -> str:
63+
"""Save the given exception's traceback in a temporary file."""
64+
if isinstance(exc, SphinxParallelError):
65+
exc_format = '(Error in parallel process)\n' + exc.traceback
66+
else:
67+
import traceback
68+
69+
exc_format = traceback.format_exc()
70+
71+
last_msgs = exts_list = ''
72+
if app is not None:
73+
extensions = app.extensions.values()
74+
last_msgs = '\n'.join(f'* {strip_colors(s)}' for s in app.messagelog)
75+
exts_list = '\n'.join(f'* {ext.name} ({ext.version})' for ext in extensions
76+
if ext.version != 'builtin')
77+
78+
with tempfile.NamedTemporaryFile(suffix='.log', prefix='sphinx-err-', delete=False) as f:
79+
f.write(error_info(last_msgs, exts_list, exc_format).encode('utf-8'))
80+
81+
return f.name
82+
83+
84+
def handle_exception(
85+
exception: BaseException,
86+
/,
87+
*,
88+
stderr: TextIO = sys.stderr,
89+
use_pdb: bool = False,
90+
print_traceback: bool = False,
91+
app: Sphinx | None = None,
92+
) -> None:
93+
from bdb import BdbQuit
94+
from traceback import TracebackException, print_exc
95+
96+
from docutils.utils import SystemMessage
97+
98+
from sphinx._cli.util.colour import red
99+
from sphinx.errors import SphinxError
100+
from sphinx.locale import __
101+
102+
if isinstance(exception, BdbQuit):
103+
return
104+
105+
def print_err(*values: str) -> None:
106+
print(*values, file=stderr)
107+
108+
def print_red(*values: str) -> None:
109+
print_err(*map(red, values))
110+
111+
print_err()
112+
if print_traceback or use_pdb:
113+
print_exc(file=stderr)
114+
print_err()
115+
116+
if use_pdb:
117+
from pdb import post_mortem
118+
119+
print_red(__('Exception occurred, starting debugger:'))
120+
post_mortem()
121+
return
122+
123+
if isinstance(exception, KeyboardInterrupt):
124+
print_err(__('Interrupted!'))
125+
return
126+
127+
if isinstance(exception, SystemMessage):
128+
print_red(__('reStructuredText markup error:'))
129+
print_err(str(exception))
130+
return
131+
132+
if isinstance(exception, SphinxError):
133+
print_red(f'{exception.category}:')
134+
print_err(str(exception))
135+
return
136+
137+
if isinstance(exception, UnicodeError):
138+
print_red(__('Encoding error:'))
139+
print_err(str(exception))
140+
return
141+
142+
if isinstance(exception, RecursionError):
143+
print_red(__('Recursion error:'))
144+
print_err(str(exception))
145+
print_err()
146+
print_err(__('This can happen with very large or deeply nested source '
147+
'files. You can carefully increase the default Python '
148+
'recursion limit of 1000 in conf.py with e.g.:'))
149+
print_err('\n import sys\n sys.setrecursionlimit(1_500)\n')
150+
return
151+
152+
# format an exception with traceback, but only the last frame.
153+
te = TracebackException.from_exception(exception, limit=-1)
154+
formatted_tb = te.stack.format()[-1] + ''.join(te.format_exception_only()).rstrip()
155+
156+
print_red(__('Exception occurred:'))
157+
print_err(formatted_tb)
158+
traceback_info_path = save_traceback(app, exception)
159+
print_err(__('The full traceback has been saved in:'))
160+
print_err(traceback_info_path)
161+
print_err()
162+
print_err(__('To report this error to the developers, please open an issue '
163+
'at <https://github.com/sphinx-doc/sphinx/issues/>. Thanks!'))
164+
print_err(__('Please also report this if it was a user error, so '
165+
'that a better error message can be provided next time.'))

0 commit comments

Comments
 (0)