1"""sys.excepthook for IPython itself, leaves a detailed report on disk.
2
3Authors:
4
5* Fernando Perez
6* Brian E. Granger
7"""
8
9#-----------------------------------------------------------------------------
10# Copyright (C) 2001-2007 Fernando Perez. <fperez@colorado.edu>
11# Copyright (C) 2008-2011 The IPython Development Team
12#
13# Distributed under the terms of the BSD License. The full license is in
14# the file COPYING, distributed as part of this software.
15#-----------------------------------------------------------------------------
16
17#-----------------------------------------------------------------------------
18# Imports
19#-----------------------------------------------------------------------------
20
21import sys
22import traceback
23from pprint import pformat
24from pathlib import Path
25
26import builtins as builtin_mod
27
28from IPython.core import ultratb
29from IPython.core.application import Application
30from IPython.core.release import author_email
31from IPython.utils.sysinfo import sys_info
32
33from IPython.core.release import __version__ as version
34
35from typing import Optional, Dict
36import types
37
38#-----------------------------------------------------------------------------
39# Code
40#-----------------------------------------------------------------------------
41
42# Template for the user message.
43_default_message_template = """\
44Oops, {app_name} crashed. We do our best to make it stable, but...
45
46A crash report was automatically generated with the following information:
47 - A verbatim copy of the crash traceback.
48 - A copy of your input history during this session.
49 - Data on your current {app_name} configuration.
50
51It was left in the file named:
52\t'{crash_report_fname}'
53If you can email this file to the developers, the information in it will help
54them in understanding and correcting the problem.
55
56You can mail it to: {contact_name} at {contact_email}
57with the subject '{app_name} Crash Report'.
58
59If you want to do it now, the following command will work (under Unix):
60mail -s '{app_name} Crash Report' {contact_email} < {crash_report_fname}
61
62In your email, please also include information about:
63- The operating system under which the crash happened: Linux, macOS, Windows,
64 other, and which exact version (for example: Ubuntu 16.04.3, macOS 10.13.2,
65 Windows 10 Pro), and whether it is 32-bit or 64-bit;
66- How {app_name} was installed: using pip or conda, from GitHub, as part of
67 a Docker container, or other, providing more detail if possible;
68- How to reproduce the crash: what exact sequence of instructions can one
69 input to get the same crash? Ideally, find a minimal yet complete sequence
70 of instructions that yields the crash.
71
72To ensure accurate tracking of this issue, please file a report about it at:
73{bug_tracker}
74"""
75
76_lite_message_template = """
77If you suspect this is an IPython {version} bug, please report it at:
78 https://github.com/ipython/ipython/issues
79or send an email to the mailing list at {email}
80
81You can print a more detailed traceback right now with "%tb", or use "%debug"
82to interactively debug it.
83
84Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
85 {config}Application.verbose_crash=True
86"""
87
88
89class CrashHandler:
90 """Customizable crash handlers for IPython applications.
91
92 Instances of this class provide a :meth:`__call__` method which can be
93 used as a ``sys.excepthook``. The :meth:`__call__` signature is::
94
95 def __call__(self, etype, evalue, etb)
96 """
97
98 message_template = _default_message_template
99 section_sep = '\n\n'+'*'*75+'\n\n'
100 info: Dict[str, Optional[str]]
101
102 def __init__(
103 self,
104 app: Application,
105 contact_name: Optional[str] = None,
106 contact_email: Optional[str] = None,
107 bug_tracker: Optional[str] = None,
108 show_crash_traceback: bool = True,
109 call_pdb: bool = False,
110 ):
111 """Create a new crash handler
112
113 Parameters
114 ----------
115 app : Application
116 A running :class:`Application` instance, which will be queried at
117 crash time for internal information.
118 contact_name : str
119 A string with the name of the person to contact.
120 contact_email : str
121 A string with the email address of the contact.
122 bug_tracker : str
123 A string with the URL for your project's bug tracker.
124 show_crash_traceback : bool
125 If false, don't print the crash traceback on stderr, only generate
126 the on-disk report
127 call_pdb
128 Whether to call pdb on crash
129
130 Attributes
131 ----------
132 These instances contain some non-argument attributes which allow for
133 further customization of the crash handler's behavior. Please see the
134 source for further details.
135
136 """
137 self.crash_report_fname = "Crash_report_%s.txt" % app.name
138 self.app = app
139 self.call_pdb = call_pdb
140 #self.call_pdb = True # dbg
141 self.show_crash_traceback = show_crash_traceback
142 self.info = dict(app_name = app.name,
143 contact_name = contact_name,
144 contact_email = contact_email,
145 bug_tracker = bug_tracker,
146 crash_report_fname = self.crash_report_fname)
147
148 def __call__(
149 self,
150 etype: type[BaseException],
151 evalue: BaseException,
152 etb: types.TracebackType,
153 ) -> None:
154 """Handle an exception, call for compatible with sys.excepthook"""
155
156 # do not allow the crash handler to be called twice without reinstalling it
157 # this prevents unlikely errors in the crash handling from entering an
158 # infinite loop.
159 sys.excepthook = sys.__excepthook__
160
161
162 # Use this ONLY for developer debugging (keep commented out for release)
163 ipython_dir = getattr(self.app, "ipython_dir", None)
164 if ipython_dir is not None:
165 assert isinstance(ipython_dir, str)
166 rptdir = Path(ipython_dir)
167 else:
168 rptdir = Path.cwd()
169 if not rptdir.is_dir():
170 rptdir = Path.cwd()
171 report_name = rptdir / self.crash_report_fname
172 # write the report filename into the instance dict so it can get
173 # properly expanded out in the user message template
174 self.crash_report_fname = str(report_name)
175 self.info["crash_report_fname"] = str(report_name)
176 TBhandler = ultratb.VerboseTB(
177 theme_name="nocolor",
178 long_header=True,
179 call_pdb=self.call_pdb,
180 )
181 if self.call_pdb:
182 TBhandler(etype,evalue,etb)
183 return
184 else:
185 traceback = TBhandler.text(etype,evalue,etb,context=31)
186
187 # print traceback to screen
188 if self.show_crash_traceback:
189 print(traceback, file=sys.stderr)
190
191 # and generate a complete report on disk
192 try:
193 report = open(report_name, "w", encoding="utf-8")
194 except:
195 print('Could not create crash report on disk.', file=sys.stderr)
196 return
197
198 with report:
199 # Inform user on stderr of what happened
200 print('\n'+'*'*70+'\n', file=sys.stderr)
201 print(self.message_template.format(**self.info), file=sys.stderr)
202
203 # Construct report on disk
204 report.write(self.make_report(str(traceback)))
205
206 builtin_mod.input("Hit <Enter> to quit (your terminal may close):")
207
208 def make_report(self, traceback: str) -> str:
209 """Return a string containing a crash report."""
210
211 sec_sep = self.section_sep
212
213 report = ['*'*75+'\n\n'+'IPython post-mortem report\n\n']
214 rpt_add = report.append
215 rpt_add(sys_info())
216
217 try:
218 config = pformat(self.app.config)
219 rpt_add(sec_sep)
220 rpt_add("Application name: %s\n\n" % self.app.name)
221 rpt_add("Current user configuration structure:\n\n")
222 rpt_add(config)
223 except:
224 pass
225 rpt_add(sec_sep+'Crash traceback:\n\n' + traceback)
226
227 return ''.join(report)
228
229
230def crash_handler_lite(
231 etype: type[BaseException], evalue: BaseException, tb: types.TracebackType
232) -> None:
233 """a light excepthook, adding a small message to the usual traceback"""
234 traceback.print_exception(etype, evalue, tb)
235
236 from IPython.core.interactiveshell import InteractiveShell
237 if InteractiveShell.initialized():
238 # we are in a Shell environment, give %magic example
239 config = "%config "
240 else:
241 # we are not in a shell, show generic config
242 config = "c."
243 print(_lite_message_template.format(email=author_email, config=config, version=version), file=sys.stderr)
244