Skip to content

Commit 5169f0f

Browse files
committed
Add _cli.util.errors
1 parent 67d0477 commit 5169f0f

File tree

2 files changed

+168
-14
lines changed

2 files changed

+168
-14
lines changed

sphinx/_cli/util/console.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

sphinx/_cli/util/errors.py

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

0 commit comments

Comments
 (0)