├── .github └── workflows │ ├── black.yml │ ├── main.yml │ └── publish.yml ├── .gitignore ├── License.txt ├── MANIFEST.in ├── README.rst ├── __init__.py ├── better_exchook.py ├── demo.py ├── pyproject.toml ├── setup.py └── test.py /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: black 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | branches: 10 | - main 11 | - master 12 | 13 | jobs: 14 | check-black-formatting: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.13 21 | - run: pip install black==22.3.0 22 | - run: black --diff . 23 | - run: black --check . 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: 15 | - 3.8 16 | - 3.9 17 | - 3.13 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Test Python version. 27 | run: | 28 | echo "Python env: $(type python) $(python --version)" 29 | 30 | - name: Run test 31 | run: | 32 | python test.py 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI"] 6 | branches: [master] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | publish: 12 | if: >- 13 | github.event.workflow_run.conclusion == 'success' && 14 | github.event.workflow_run.head_branch == 'master' && 15 | github.event.workflow_run.event == 'push' && 16 | github.repository == 'albertz/py_better_exchook' 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.13 24 | 25 | - name: Install Python deps 26 | run: | 27 | echo "PATH=$PATH:$HOME/.local/bin" >> $GITHUB_ENV 28 | pip3 install --user --upgrade pip build twine 29 | 30 | - run: python3 -m build 31 | 32 | # https://github.com/marketplace/actions/pypi-publish 33 | - name: Publish to PyPI 34 | # https://github.com/pypa/gh-action-pypi-publish/issues/112 35 | uses: pypa/gh-action-pypi-publish@release/v1.4 36 | with: 37 | user: __token__ 38 | password: ${{ secrets.pypi_password }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .uuid 3 | 4 | # via setuptools 5 | /MANIFEST 6 | /build 7 | /dist 8 | 9 | # PyCharm 10 | /.idea 11 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Albert Zeyer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | better_exchook 3 | ============== 4 | 5 | A nicer drop-in-replacement for Python ``sys.excepthook``, 6 | i.e. it prints stack traces with extended information. 7 | It will add some useful information for each frame, 8 | like printing the relevant variables (relevant = referenced in the code line). 9 | Also see `Python source and comments `_ for further details. 10 | 11 | Features 12 | -------- 13 | * Shows locals/globals per frame, but only those used in the current statement. 14 | It does this by a simple Python code parser. 15 | * Multi-line Python statements in the stack trace output, 16 | in case the statement goes over multiple lines. 17 | * Shows full function qualified name (not just ``co_name``). 18 | * Colored/formatted output of each frame. 19 | * Syntax highlighting for the Python source code. 20 | * Support for `DomTerm `__ text folding 21 | (`see more `__), 22 | where it folds all the details of each stack frame away by default, 23 | and thus provides a much more comprehensive overview, 24 | while still providing all the details when needed. 25 | 26 | .. image:: https://github.com/albertz/py_better_exchook/workflows/CI/badge.svg 27 | :target: https://github.com/albertz/py_better_exchook/actions 28 | 29 | 30 | Installation 31 | ------------ 32 | 33 | You can just copy over the single file ``better_exchook.py`` to your project. 34 | 35 | Or alternatively, it is also available `on PyPI `_ 36 | and can be installed via: 37 | 38 | .. code:: 39 | 40 | pip install better_exchook 41 | 42 | 43 | Usage 44 | ----- 45 | 46 | .. code:: python 47 | 48 | import better_exchook 49 | better_exchook.install() # will just do: sys.excepthook = better_exchook 50 | 51 | Or: 52 | 53 | .. code:: python 54 | 55 | import better_exchook 56 | better_exchook.setup_all() 57 | 58 | * **setup_all** 59 | - ``install`` + ``replace_traceback_format_tb`` + ``replace_traceback_print_tb`` 60 | * **install**: 61 | - ``sys.excepthook = better_exchook`` 62 | * **replace_traceback_format_tb**: 63 | - ``traceback.format_tb = format_tb`` 64 | - ``traceback.StackSummary.format = format_tb`` 65 | - ``traceback.StackSummary.extract = _StackSummary_extract`` 66 | * **replace_traceback_print_tb**: 67 | - ``traceback.print_tb = print_tb`` 68 | - ``traceback.print_exception = print_exception`` 69 | - ``traceback.print_exc = print_exc`` 70 | 71 | 72 | Examples 73 | -------- 74 | 75 | Python example code: 76 | 77 | .. code:: python 78 | 79 | try: 80 | x = {1:2, "a":"b"} 81 | def f(): 82 | y = "foo" 83 | x, 42, sys.stdin.__class__, sys.exc_info, y, z 84 | f() 85 | except Exception: 86 | better_exchook.better_exchook(*sys.exc_info()) 87 | 88 | Output: 89 | 90 | .. code:: 91 | 92 | EXCEPTION 93 | Traceback (most recent call last): 94 | File "better_exchook.py", line 478, in 95 | line: f() 96 | locals: 97 | f = 98 | File "better_exchook.py", line 477, in f 99 | line: x, 42, sys.stdin.__class__, sys.exc_info, y, z 100 | locals: 101 | x = {'a': 'b', 1: 2} 102 | sys = 103 | sys.stdin = ', mode 'r' at 0x107d9f0c0> 104 | sys.stdin.__class__ = 105 | sys.exc_info = 106 | y = 'foo' 107 | z = 108 | NameError: global name 'z' is not defined 109 | 110 | Python example code: 111 | 112 | .. code:: python 113 | 114 | try: 115 | f = lambda x: None 116 | f(x, y) 117 | except Exception: 118 | better_exchook.better_exchook(*sys.exc_info()) 119 | 120 | Output: 121 | 122 | .. code:: 123 | 124 | EXCEPTION 125 | Traceback (most recent call last): 126 | File "better_exchook.py", line 484, in 127 | line: f(x, y) 128 | locals: 129 | f = at 0x107f1df50> 130 | x = {'a': 'b', 1: 2} 131 | y = 132 | NameError: name 'y' is not defined 133 | 134 | Python example code: 135 | 136 | .. code:: python 137 | 138 | try: 139 | (lambda x: None)(__name__, 140 | 42) # multiline 141 | except Exception: 142 | better_exchook.better_exchook(*sys.exc_info()) 143 | 144 | Output: 145 | 146 | .. code:: 147 | 148 | EXCEPTION 149 | Traceback (most recent call last): 150 | File "better_exchook.py", line 490, in 151 | line: (lambda x: None)(__name__, 152 | 42) # multiline 153 | locals: 154 | x = {'a': 'b', 1: 2} 155 | __name__ = '__main__', len = 8 156 | TypeError: () takes exactly 1 argument (2 given) 157 | 158 | Python example code: 159 | 160 | .. code:: python 161 | 162 | # use this to overwrite the global exception handler 163 | sys.excepthook = better_exchook.better_exchook 164 | # and fail 165 | finalfail(sys) 166 | 167 | Output: 168 | 169 | .. code:: 170 | 171 | EXCEPTION 172 | Traceback (most recent call last): 173 | File "better_exchook.py", line 497, in 174 | line: finalfail(sys) 175 | locals: 176 | finalfail = 177 | sys = 178 | NameError: name 'finalfail' is not defined 179 | 180 | Screenshot: 181 | 182 | .. image:: https://gist.githubusercontent.com/albertz/a4ce78e5ccd037041638777f10b10327/raw/7ec2bb7079dbd56119d498f20905404cb2d812c0/screenshot1.png 183 | 184 | .. _domterm: 185 | 186 | Screencast with `DomTerm `__ using text folding (`see more `__): 187 | 188 | .. image:: https://gist.githubusercontent.com/albertz/a4ce78e5ccd037041638777f10b10327/raw/7ec2bb7079dbd56119d498f20905404cb2d812c0/screencast-domterm.gif 189 | 190 | 191 | Similar projects 192 | ---------------- 193 | 194 | * `Nose does something similar for assertion failures `_. 195 | * IPython has something similar (`ultratb `__). 196 | Do this: ``from IPython.core import ultratb; sys.excepthook = ultratb.VerboseTB()``. 197 | Shows more source code context (but not necessarily all relevant parts). 198 | * Ka-Ping Yee's "cgitb.py", which is part of Python, 199 | `see here `__, 200 | `code here `__. 201 | * `Rich Python library `__. 202 | Syntax highlighting but without locals. 203 | * `andy-landy / traceback_with_variables `__. 204 | Python Traceback (Error Message) Printing Variables. 205 | Very similar, but less advanced. 206 | Only shows locals, not globals, and also just all locals, not only those used in current statement. 207 | Also does not expand statement if it goes over multiple lines. 208 | * `cknd / stackprinter `__. 209 | Similar as IPython ultratb. 210 | * `patrys / great-justice `_ 211 | * See `this `__ 212 | related StackOverflow question. 213 | 214 | 215 | -- Albert Zeyer, 216 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .better_exchook import * 2 | -------------------------------------------------------------------------------- /better_exchook.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2011-2021, Albert Zeyer, www.az2000.de 3 | # All rights reserved. 4 | # file created 2011-04-15 5 | 6 | 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # 1. Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 2. Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | """ 28 | https://github.com/albertz/py_better_exchook 29 | 30 | This is a simple replacement for the standard Python exception handler (sys.excepthook). 31 | In addition to what the standard handler does, it also prints all referenced variables 32 | (no matter if local, global or builtin) of the code line of each stack frame. 33 | See below for some examples and some example output. 34 | 35 | See these functions: 36 | 37 | - better_exchook 38 | - format_tb / print_tb 39 | - iter_traceback 40 | - get_current_frame 41 | - dump_all_thread_tracebacks 42 | - install 43 | - setup_all 44 | - replace_traceback_format_tb 45 | - replace_traceback_print_tb 46 | 47 | Although there might be a few more useful functions, thus we export all of them. 48 | 49 | Also see the demo/tests at the end. 50 | """ 51 | 52 | from __future__ import annotations 53 | 54 | import sys 55 | import os 56 | import os.path 57 | import threading 58 | import keyword 59 | import inspect 60 | import contextlib 61 | from weakref import WeakKeyDictionary 62 | 63 | try: 64 | import typing 65 | except ImportError: 66 | typing = None 67 | 68 | try: 69 | from traceback import StackSummary, FrameSummary 70 | except ImportError: # StackSummary, FrameSummary were added in Python 3.5 71 | 72 | class _Dummy: 73 | pass 74 | 75 | StackSummary = FrameSummary = _Dummy 76 | 77 | # noinspection PySetFunctionToLiteral,SpellCheckingInspection 78 | py_keywords = set(keyword.kwlist) | set(["None", "True", "False"]) 79 | 80 | _cur_pwd = os.getcwd() 81 | _threading_main_thread = threading.main_thread() if hasattr(threading, "main_thread") else None 82 | 83 | try: 84 | # noinspection PyUnresolvedReferences,PyUnboundLocalVariable 85 | unicode 86 | except NameError: # Python3 87 | unicode = str # Python 3 compatibility 88 | 89 | try: 90 | # noinspection PyUnresolvedReferences,PyUnboundLocalVariable 91 | raw_input 92 | except NameError: # Python3 93 | raw_input = input 94 | 95 | 96 | PY3 = sys.version_info[0] >= 3 97 | 98 | 99 | def parse_py_statement(line): 100 | """ 101 | Parse Python statement into tokens. 102 | Note that this is incomplete. 103 | It should be simple and fast and just barely enough for what we need here. 104 | 105 | Reference: 106 | https://docs.python.org/3/reference/lexical_analysis.html 107 | 108 | :param str line: 109 | :return: yields (type, value) 110 | :rtype: typing.Iterator[typing.Tuple[str,str]] 111 | """ 112 | state = 0 113 | cur_token = "" 114 | spaces = " \t\n" 115 | ops = ".,;:+-*/%&!=|(){}[]^<>" 116 | i = 0 117 | 118 | def _escape_char(_c): 119 | if _c == "n": 120 | return "\n" 121 | elif _c == "t": 122 | return "\t" 123 | else: 124 | return _c 125 | 126 | while i < len(line): 127 | c = line[i] 128 | i += 1 129 | if state == 0: 130 | if c in spaces: 131 | pass 132 | elif c in ops: 133 | yield "op", c 134 | elif c == "#": 135 | state = 6 136 | elif c == '"': 137 | state = 1 138 | elif c == "'": 139 | state = 2 140 | else: 141 | cur_token = c 142 | state = 3 143 | elif state == 1: # string via " 144 | if c == "\\": 145 | state = 4 146 | elif c == '"': 147 | yield "str", cur_token 148 | cur_token = "" 149 | state = 0 150 | else: 151 | cur_token += c 152 | elif state == 2: # string via ' 153 | if c == "\\": 154 | state = 5 155 | elif c == "'": 156 | yield "str", cur_token 157 | cur_token = "" 158 | state = 0 159 | else: 160 | cur_token += c 161 | elif state == 3: # identifier 162 | if c in spaces + ops + "#": 163 | yield "id", cur_token 164 | cur_token = "" 165 | state = 0 166 | i -= 1 167 | elif c == '"': # identifier is string prefix 168 | cur_token = "" 169 | state = 1 170 | elif c == "'": # identifier is string prefix 171 | cur_token = "" 172 | state = 2 173 | else: 174 | cur_token += c 175 | elif state == 4: # escape in " 176 | cur_token += _escape_char(c) 177 | state = 1 178 | elif state == 5: # escape in ' 179 | cur_token += _escape_char(c) 180 | state = 2 181 | elif state == 6: # comment 182 | cur_token += c 183 | if state == 3: 184 | yield "id", cur_token 185 | elif state == 6: 186 | yield "comment", cur_token 187 | 188 | 189 | def parse_py_statements(source_code): 190 | """ 191 | :param str source_code: 192 | :return: via :func:`parse_py_statement` 193 | :rtype: typing.Iterator[typing.Tuple[str,str]] 194 | """ 195 | for line in source_code.splitlines(): 196 | for t in parse_py_statement(line): 197 | yield t 198 | 199 | 200 | def grep_full_py_identifiers(tokens): 201 | """ 202 | :param typing.Iterable[(str,str)] tokens: 203 | :rtype: typing.Iterator[str] 204 | """ 205 | global py_keywords 206 | tokens = list(tokens) 207 | i = 0 208 | while i < len(tokens): 209 | token_type, token = tokens[i] 210 | i += 1 211 | if token_type != "id": 212 | continue 213 | while i + 1 < len(tokens) and tokens[i] == ("op", ".") and tokens[i + 1][0] == "id": 214 | token += "." + tokens[i + 1][1] 215 | i += 2 216 | if token == "": 217 | continue 218 | if token in py_keywords: 219 | continue 220 | if token[0] in ".0123456789": 221 | continue 222 | yield token 223 | 224 | 225 | def set_linecache(filename, source): 226 | """ 227 | The :mod:`linecache` module has some cache of the source code for the current source. 228 | Sometimes it fails to find the source of some files. 229 | We can explicitly set the source for some filename. 230 | 231 | :param str filename: 232 | :param str source: 233 | :return: nothing 234 | """ 235 | import linecache 236 | 237 | linecache.cache[filename] = None, None, [line + "\n" for line in source.splitlines()], filename 238 | 239 | 240 | # noinspection PyShadowingBuiltins 241 | def simple_debug_shell(globals, locals): 242 | """ 243 | :param dict[str,typing.Any] globals: 244 | :param dict[str,typing.Any] locals: 245 | :return: nothing 246 | """ 247 | try: 248 | import readline 249 | except ImportError: 250 | pass # ignore 251 | compile_string_fn = "" 252 | while True: 253 | try: 254 | s = raw_input("> ") 255 | except (KeyboardInterrupt, EOFError): 256 | print("breaked debug shell: " + sys.exc_info()[0].__name__) 257 | break 258 | if s.strip() == "": 259 | continue 260 | try: 261 | c = compile(s, compile_string_fn, "single") 262 | except Exception as e: 263 | print("%s : %s in %r" % (e.__class__.__name__, str(e), s)) 264 | else: 265 | set_linecache(compile_string_fn, s) 266 | # noinspection PyBroadException 267 | try: 268 | ret = eval(c, globals, locals) 269 | except (KeyboardInterrupt, SystemExit): 270 | print("debug shell exit: " + sys.exc_info()[0].__name__) 271 | break 272 | except Exception: 273 | print("Error executing %r" % s) 274 | better_exchook(*sys.exc_info(), autodebugshell=False) 275 | else: 276 | # noinspection PyBroadException 277 | try: 278 | if ret is not None: 279 | print(ret) 280 | except Exception: 281 | print("Error printing return value of %r" % s) 282 | better_exchook(*sys.exc_info(), autodebugshell=False) 283 | 284 | 285 | # keep non-PEP8 argument name for compatibility 286 | # noinspection PyPep8Naming 287 | def debug_shell(user_ns, user_global_ns, traceback=None, execWrapper=None): 288 | """ 289 | Spawns some interactive shell. Tries to use IPython if available. 290 | Falls back to :func:`pdb.post_mortem` or :func:`simple_debug_shell`. 291 | 292 | :param dict[str,typing.Any] user_ns: 293 | :param dict[str,typing.Any] user_global_ns: 294 | :param traceback: 295 | :param execWrapper: 296 | :return: nothing 297 | """ 298 | ipshell = None 299 | try: 300 | # noinspection PyPackageRequirements 301 | import IPython 302 | 303 | have_ipython = True 304 | except ImportError: 305 | have_ipython = False 306 | 307 | if not ipshell and traceback and have_ipython: 308 | # noinspection PyBroadException 309 | try: 310 | # noinspection PyPackageRequirements,PyUnresolvedReferences 311 | from IPython.core.debugger import Pdb 312 | 313 | # noinspection PyPackageRequirements,PyUnresolvedReferences 314 | from IPython.terminal.debugger import TerminalPdb 315 | 316 | # noinspection PyPackageRequirements,PyUnresolvedReferences 317 | from IPython.terminal.ipapp import TerminalIPythonApp 318 | 319 | ipapp = TerminalIPythonApp.instance() 320 | ipapp.interact = False # Avoid output (banner, prints) 321 | ipapp.initialize(argv=[]) 322 | def_colors = ipapp.shell.colors 323 | pdb_obj = TerminalPdb(def_colors) 324 | pdb_obj.botframe = None # not sure. exception otherwise at quit 325 | 326 | def ipshell(): 327 | """ 328 | Run the IPython shell. 329 | """ 330 | pdb_obj.interaction(None, traceback) 331 | 332 | except Exception: 333 | print("IPython Pdb exception:") 334 | better_exchook(*sys.exc_info(), autodebugshell=False, file=sys.stdout) 335 | 336 | if not ipshell and have_ipython: 337 | # noinspection PyBroadException 338 | try: 339 | # noinspection PyPackageRequirements,PyUnresolvedReferences 340 | import IPython 341 | 342 | # noinspection PyPackageRequirements,PyUnresolvedReferences 343 | import IPython.terminal.embed 344 | 345 | class DummyMod: 346 | """Dummy module""" 347 | 348 | module = DummyMod() 349 | module.__dict__ = user_global_ns 350 | module.__name__ = "_DummyMod" 351 | if "__name__" not in user_ns: 352 | user_ns = user_ns.copy() 353 | user_ns["__name__"] = "_DummyUserNsMod" 354 | ipshell = IPython.terminal.embed.InteractiveShellEmbed.instance(user_ns=user_ns, user_module=module) 355 | except Exception: 356 | print("IPython not available:") 357 | better_exchook(*sys.exc_info(), autodebugshell=False, file=sys.stdout) 358 | else: 359 | if execWrapper: 360 | old = ipshell.run_code 361 | ipshell.run_code = lambda code: execWrapper(lambda: old(code)) 362 | if ipshell: 363 | ipshell() 364 | else: 365 | print("Use simple pdb debug shell:") 366 | if traceback: 367 | import pdb 368 | 369 | pdb.post_mortem(traceback) 370 | else: 371 | simple_debug_shell(user_global_ns, user_ns) 372 | 373 | 374 | def output_limit(): 375 | """ 376 | :return: num chars 377 | :rtype: int 378 | """ 379 | return 300 380 | 381 | 382 | def fallback_findfile(filename): 383 | """ 384 | :param str filename: 385 | :return: try to find the full filename, e.g. in modules, etc 386 | :rtype: str|None 387 | """ 388 | mods = [m for m in list(sys.modules.values()) if m and getattr(m, "__file__", None) and filename in m.__file__] 389 | if len(mods) == 0: 390 | return None 391 | alt_fn = mods[0].__file__ 392 | if alt_fn[-4:-1] == ".py": 393 | alt_fn = alt_fn[:-1] # *.pyc or whatever 394 | if not os.path.exists(alt_fn) and alt_fn.startswith("./"): 395 | # Maybe current dir changed. 396 | alt_fn2 = _cur_pwd + alt_fn[1:] 397 | if os.path.exists(alt_fn2): 398 | return alt_fn2 399 | # Try dirs of some other mods. 400 | for m in ["__main__", "better_exchook"]: 401 | if hasattr(sys.modules.get(m), "__file__"): 402 | alt_fn2 = os.path.dirname(sys.modules[m].__file__) + alt_fn[1:] 403 | if os.path.exists(alt_fn2): 404 | return alt_fn2 405 | return alt_fn 406 | 407 | 408 | def is_source_code_missing_brackets(source_code, prioritize_missing_open=False): 409 | """ 410 | We check whether this source code snippet (e.g. one line) is complete/even w.r.t. opening/closing brackets. 411 | 412 | :param str source_code: 413 | :param bool prioritize_missing_open: once we found any missing open bracket, directly return -1 414 | :return: 1 if missing_close, -1 if missing_open, 0 otherwise. 415 | I.e. whether there are missing open/close brackets. 416 | E.g. this would mean that you might want to include the prev/next source code line as well in the stack trace. 417 | :rtype: int 418 | """ 419 | open_brackets = "[{(" 420 | close_brackets = "]})" 421 | last_bracket = [-1] # stack 422 | counters = [0] * len(open_brackets) 423 | missing_open = False 424 | for t_type, t_content in list(parse_py_statements(source_code)): 425 | if t_type != "op": 426 | continue # we are from now on only interested in ops (including brackets) 427 | if t_content in open_brackets: 428 | idx = open_brackets.index(t_content) 429 | counters[idx] += 1 430 | last_bracket.append(idx) 431 | elif t_content in close_brackets: 432 | idx = close_brackets.index(t_content) 433 | if last_bracket[-1] == idx: 434 | counters[idx] -= 1 435 | del last_bracket[-1] 436 | else: 437 | if prioritize_missing_open: 438 | return -1 439 | missing_open = True 440 | missing_close = not all([c == 0 for c in counters]) 441 | if missing_close: 442 | return 1 443 | if missing_open: 444 | return -1 445 | return 0 446 | 447 | 448 | def is_source_code_missing_open_brackets(source_code): 449 | """ 450 | We check whether this source code snippet (e.g. one line) is complete/even w.r.t. opening/closing brackets. 451 | 452 | :param str source_code: 453 | :return: whether there are missing open brackets. 454 | E.g. this would mean that you might want to include the previous source code line as well in the stack trace. 455 | :rtype: bool 456 | """ 457 | return is_source_code_missing_brackets(source_code, prioritize_missing_open=True) < 0 458 | 459 | 460 | def get_source_code(filename, lineno, module_globals=None): 461 | """ 462 | :param str filename: 463 | :param int lineno: 464 | :param dict[str,typing.Any]|None module_globals: 465 | :return: source code of that line (including newline) 466 | :rtype: str 467 | """ 468 | import linecache 469 | 470 | linecache.checkcache(filename) 471 | source_code = linecache.getline(filename, lineno, module_globals) 472 | # In case of a multi-line statement, lineno is usually the last line. 473 | # We are checking for missing open brackets and add earlier code lines. 474 | start_line = end_line = lineno 475 | lines = None 476 | while True: 477 | missing_bracket_level = is_source_code_missing_brackets(source_code) 478 | if missing_bracket_level == 0: 479 | break 480 | if not lines: 481 | lines = linecache.getlines(filename, module_globals) 482 | if missing_bracket_level < 0: # missing open bracket, add prev line 483 | start_line -= 1 484 | if start_line < 1: # 1-indexed 485 | break 486 | else: 487 | end_line += 1 488 | if end_line > len(lines): # 1-indexed 489 | break 490 | source_code = "".join(lines[start_line - 1 : end_line]) # 1-indexed 491 | return source_code 492 | 493 | 494 | def str_visible_len(s): 495 | """ 496 | :param str s: 497 | :return: len without escape chars 498 | :rtype: int 499 | """ 500 | import re 501 | 502 | # via: https://github.com/chalk/ansi-regex/blob/master/index.js 503 | s = re.sub("[\x1b\x9b][\\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]", "", s) 504 | return len(s) 505 | 506 | 507 | def add_indent_lines(prefix, s): 508 | """ 509 | :param str prefix: 510 | :param str s: 511 | :return: s with prefix indent added to all lines 512 | :rtype: str 513 | """ 514 | if not s: 515 | return prefix 516 | prefix_len = str_visible_len(prefix) 517 | lines = s.splitlines(True) 518 | return "".join([prefix + lines[0]] + [" " * prefix_len + line for line in lines[1:]]) 519 | 520 | 521 | def get_indent_prefix(s): 522 | """ 523 | :param str s: 524 | :return: the indent spaces of s 525 | :rtype: str 526 | """ 527 | return s[: len(s) - len(s.lstrip())] 528 | 529 | 530 | def get_same_indent_prefix(lines): 531 | """ 532 | :param list[] lines: 533 | :rtype: str|None 534 | """ 535 | if not lines: 536 | return "" 537 | prefix = get_indent_prefix(lines[0]) 538 | if not prefix: 539 | return "" 540 | if all([line.startswith(prefix) for line in lines]): 541 | return prefix 542 | return None 543 | 544 | 545 | def remove_indent_lines(s): 546 | """ 547 | :param str s: 548 | :return: remove as much indentation as possible 549 | :rtype: str 550 | """ 551 | if not s: 552 | return "" 553 | lines = s.splitlines(True) 554 | prefix = get_same_indent_prefix(lines) 555 | if prefix is None: # not in expected format. just lstrip all lines 556 | return "".join([line.lstrip() for line in lines]) 557 | return "".join([line[len(prefix) :] for line in lines]) 558 | 559 | 560 | def replace_tab_indent(s, replace=" "): 561 | """ 562 | :param str s: string with tabs 563 | :param str replace: e.g. 4 spaces 564 | :rtype: str 565 | """ 566 | prefix = get_indent_prefix(s) 567 | return prefix.replace("\t", replace) + s[len(prefix) :] 568 | 569 | 570 | def replace_tab_indents(s, replace=" "): 571 | """ 572 | :param str s: multi-line string with tabs 573 | :param str replace: e.g. 4 spaces 574 | :rtype: str 575 | """ 576 | lines = s.splitlines(True) 577 | return "".join([replace_tab_indent(line, replace) for line in lines]) 578 | 579 | 580 | def to_bool(s, fallback=None): 581 | """ 582 | :param str s: str to be converted to bool, e.g. "1", "0", "true", "false" 583 | :param T fallback: if s is not recognized as a bool 584 | :return: boolean value, or fallback 585 | :rtype: bool|T 586 | """ 587 | if not s: 588 | return fallback 589 | s = s.lower() 590 | if s in ["1", "true", "yes", "y"]: 591 | return True 592 | if s in ["0", "false", "no", "n"]: 593 | return False 594 | return fallback 595 | 596 | 597 | class Color: 598 | """ 599 | Helper functions provided to perform terminal coloring. 600 | """ 601 | 602 | ColorIdxTable = { 603 | k: i for (i, k) in enumerate(["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]) 604 | } 605 | 606 | @classmethod 607 | def get_global_color_enabled(cls): 608 | """ 609 | :rtype: bool 610 | """ 611 | return to_bool(os.environ.get("CLICOLOR", ""), fallback=True) 612 | 613 | @classmethod 614 | def is_dark_terminal_background(cls): 615 | """ 616 | :return: Whether we have a dark Terminal background color, or None if unknown. 617 | We currently just check the env var COLORFGBG, 618 | which some terminals define like ":", 619 | and if in {0,1,2,3,4,5,6,8}, then we have some dark background. 620 | There are many other complex heuristics we could do here, which work in some cases but not in others. 621 | See e.g. `here `__. 622 | But instead of adding more heuristics, we think that explicitly setting COLORFGBG would be the best thing, 623 | in case it's not like you want it. 624 | :rtype: bool|None 625 | """ 626 | if os.environ.get("COLORFGBG", None): 627 | parts = os.environ["COLORFGBG"].split(";") 628 | try: 629 | last_number = int(parts[-1]) 630 | if 0 <= last_number <= 6 or last_number == 8: 631 | return True 632 | else: 633 | return False 634 | except ValueError: # not an integer? 635 | pass 636 | return None # unknown (and bool(None) == False, i.e. expect light by default) 637 | 638 | def __init__(self, enable=None): 639 | """ 640 | :param bool|None enable: 641 | """ 642 | if enable is None: 643 | enable = self.get_global_color_enabled() 644 | self.enable = enable 645 | self._dark_terminal_background = self.is_dark_terminal_background() 646 | # Set color palettes (will be used sometimes as bold, sometimes as normal). 647 | # 5 colors, for: code/general, error-msg, string, comment, line-nr. 648 | # Try to set them in a way such that if we guessed the terminal background color wrongly, 649 | # it is still not too bad (although people might disagree here...). 650 | if self._dark_terminal_background: 651 | self.fg_colors = ["yellow", "red", "cyan", "white", "magenta"] 652 | else: 653 | self.fg_colors = ["blue", "red", "cyan", "white", "magenta"] 654 | 655 | def color(self, s, color=None, bold=False): 656 | """ 657 | :param str s: 658 | :param str|None color: sth in self.ColorIdxTable 659 | :param bool bold: 660 | :return: s optionally wrapped with ansi escape codes 661 | :rtype: str 662 | """ 663 | if not self.enable: 664 | return s 665 | code_seq = [] 666 | if color: 667 | code_seq += [30 + self.ColorIdxTable[color]] # foreground color 668 | if bold: 669 | code_seq += [1] 670 | if not code_seq: 671 | return s 672 | start = "\x1b[%sm" % ";".join(map(str, code_seq)) 673 | end = "\x1b[0m" 674 | while s[:1] == " ": # move prefix spaces outside 675 | start = " " + start 676 | s = s[1:] 677 | while s[-1:] == " ": # move postfix spaces outside 678 | end += " " 679 | s = s[:-1] 680 | return start + s + end 681 | 682 | def __call__(self, *args, **kwargs): 683 | return self.color(*args, **kwargs) 684 | 685 | def py_syntax_highlight(self, s): 686 | """ 687 | :param str s: 688 | :rtype: str 689 | """ 690 | if not self.enable: 691 | return s 692 | state = 0 693 | spaces = " \t\n" 694 | ops = ".,;:+-*/%&!=|(){}[]^<>" 695 | i = 0 696 | cur_token = "" 697 | # pos in s -> color kwargs 698 | color_args = {0: {}, len(s): {}} # type: typing.Dict[int,typing.Dict[str,typing.Any]] 699 | 700 | def finish_identifier(): 701 | """ 702 | Reset color to standard for current identifier. 703 | """ 704 | if cur_token in py_keywords: 705 | color_args[max([k for k in color_args.keys() if k < i])] = {"color": self.fg_colors[0]} 706 | 707 | while i < len(s): 708 | c = s[i] 709 | i += 1 710 | if c == "\n": 711 | if state == 3: 712 | finish_identifier() 713 | color_args[i] = {} 714 | state = 0 715 | elif state == 0: 716 | if c in spaces: 717 | pass 718 | elif c in ops: 719 | color_args[i - 1] = {"color": self.fg_colors[0]} 720 | color_args[i] = {} 721 | elif c == "#": 722 | color_args[i - 1] = {"color": self.fg_colors[3]} 723 | state = 6 724 | elif c == '"': 725 | color_args[i - 1] = {"color": self.fg_colors[2]} 726 | state = 1 727 | elif c == "'": 728 | color_args[i - 1] = {"color": self.fg_colors[2]} 729 | state = 2 730 | else: 731 | cur_token = c 732 | color_args[i - 1] = {} 733 | state = 3 734 | elif state == 1: # string via " 735 | if c == "\\": 736 | state = 4 737 | elif c == '"': 738 | color_args[i] = {} 739 | state = 0 740 | elif state == 2: # string via ' 741 | if c == "\\": 742 | state = 5 743 | elif c == "'": 744 | color_args[i] = {} 745 | state = 0 746 | elif state == 3: # identifier 747 | if c in spaces + ops + "#\"'": 748 | finish_identifier() 749 | color_args[i] = {} 750 | state = 0 751 | i -= 1 752 | else: 753 | cur_token += c 754 | elif state == 4: # escape in " 755 | state = 1 756 | elif state == 5: # escape in ' 757 | state = 2 758 | elif state == 6: # comment 759 | pass 760 | if state == 3: 761 | finish_identifier() 762 | out = "" 763 | i = 0 764 | while i < len(s): 765 | j = min([k for k in color_args.keys() if k > i]) 766 | out += self.color(s[i:j], **color_args[i]) 767 | i = j 768 | return out 769 | 770 | 771 | class DomTerm: 772 | """ 773 | DomTerm (https://github.com/PerBothner/DomTerm/) is a terminal emulator 774 | with many extended escape codes, such as folding text away, or even generic HTML. 775 | We can make use of some of these features (currently just folding text). 776 | """ 777 | 778 | _is_domterm = None 779 | 780 | @classmethod 781 | def is_domterm(cls): 782 | """ 783 | :return: whether we are inside DomTerm 784 | :rtype: bool 785 | """ 786 | import os 787 | 788 | if cls._is_domterm is not None: 789 | return cls._is_domterm 790 | if not os.environ.get("DOMTERM"): 791 | cls._is_domterm = False 792 | return False 793 | cls._is_domterm = True 794 | return True 795 | 796 | @contextlib.contextmanager 797 | def logical_block(self, file=sys.stdout): 798 | """ 799 | :param io.TextIOBase|io.StringIO file: 800 | """ 801 | file.write("\033]110\007") 802 | yield 803 | file.write("\033]111\007") 804 | 805 | @contextlib.contextmanager 806 | def hide_button_span(self, mode, file=sys.stdout): 807 | """ 808 | :param int mode: 1 or 2 809 | :param io.TextIOBase|io.StringIO file: 810 | """ 811 | file.write("\033[83;%iu" % mode) 812 | yield 813 | file.write("\033[83;0u") 814 | 815 | # noinspection PyMethodMayBeStatic 816 | def indentation(self, file=sys.stdout): 817 | """ 818 | :param io.TextIOBase|io.StringIO file: 819 | """ 820 | file.write('\033]114;"│"\007') 821 | 822 | # noinspection PyMethodMayBeStatic 823 | def hide_button(self, file=sys.stdout): 824 | """ 825 | :param io.TextIOBase|io.StringIO file: 826 | """ 827 | file.write("\033[16u▶▼\033[17u") 828 | 829 | @contextlib.contextmanager 830 | def _temp_replace_attrib(self, obj, attr, new_value): 831 | old_value = getattr(obj, attr) 832 | setattr(obj, attr, new_value) 833 | yield old_value 834 | setattr(obj, attr, old_value) 835 | 836 | @contextlib.contextmanager 837 | def fold_text_stream(self, prefix, postfix="", hidden_stream=None, **kwargs): 838 | """ 839 | :param str prefix: always visible 840 | :param str postfix: always visible, right after. 841 | :param io.TextIOBase|io.StringIO hidden_stream: sys.stdout by default. 842 | If this is sys.stdout, it will replace that stream, 843 | and collect the data during the context (in the `with` block). 844 | """ 845 | import io 846 | 847 | if hidden_stream is None: 848 | hidden_stream = sys.stdout 849 | assert isinstance(hidden_stream, io.IOBase) 850 | assert hidden_stream is sys.stdout, "currently not supported otherwise" 851 | hidden_buf = io.StringIO() 852 | with self._temp_replace_attrib(sys, "stdout", hidden_buf): 853 | yield 854 | self.fold_text(prefix=prefix, postfix=postfix, hidden=hidden_buf.getvalue(), **kwargs) 855 | 856 | def fold_text(self, prefix, hidden, postfix="", file=None, align=0): 857 | """ 858 | :param str prefix: always visible 859 | :param str hidden: hidden 860 | If this is sys.stdout, it will replace that stream, 861 | and collect the data during the context (in the `with` block). 862 | :param str postfix: always visible, right after. "" by default. 863 | :param io.TextIOBase|io.StringIO file: sys.stdout by default. 864 | :param int align: remove this number of initial chars from hidden 865 | """ 866 | if file is None: 867 | file = sys.stdout 868 | # Extra logic: Multi-line hidden. Add initial "\n" if not there. 869 | if "\n" in hidden: 870 | if hidden[:1] != "\n": 871 | hidden = "\n" + hidden 872 | # Extra logic: A final "\n" of hidden, make it always visible such that it looks nicer. 873 | if hidden[-1:] == "\n": 874 | hidden = hidden[:-1] 875 | postfix += "\n" 876 | if self.is_domterm(): 877 | with self.logical_block(file=file): 878 | self.indentation(file=file) 879 | self.hide_button(file=file) 880 | file.write(prefix) 881 | if prefix.endswith("\x1b[0m"): 882 | file.write(" ") # bug in DomTerm? 883 | with self.hide_button_span(2, file=file): 884 | hidden_ls = hidden.split("\n") 885 | hidden_ls = [s[align:] for s in hidden_ls] 886 | hidden = "\033]118\007".join(hidden_ls) 887 | file.write(hidden) 888 | else: 889 | file.write(prefix) 890 | file.write(hidden.replace("\n", "\n ")) 891 | file.write(postfix) 892 | file.flush() 893 | 894 | def fold_text_string(self, prefix, hidden, **kwargs): 895 | """ 896 | :param str prefix: 897 | :param str hidden: 898 | :param kwargs: passed to :func:`fold_text` 899 | :rtype: str 900 | """ 901 | import io 902 | 903 | output_buf = io.StringIO() 904 | self.fold_text(prefix=prefix, hidden=hidden, file=output_buf, **kwargs) 905 | return output_buf.getvalue() 906 | 907 | 908 | def is_at_exit(): 909 | """ 910 | Some heuristics to figure out whether this is called at a stage where the Python interpreter is shutting down. 911 | 912 | :return: whether the Python interpreter is currently in the process of shutting down 913 | :rtype: bool 914 | """ 915 | if _threading_main_thread is not None: 916 | if not hasattr(threading, "main_thread"): 917 | return True 918 | if threading.main_thread() != _threading_main_thread: 919 | return True 920 | if not _threading_main_thread.is_alive(): 921 | return True 922 | return False 923 | 924 | 925 | class _OutputLinesCollector: 926 | def __init__(self, color): 927 | """ 928 | :param Color color: 929 | """ 930 | self.color = color 931 | self.lines = [] 932 | self.dom_term = DomTerm() if DomTerm.is_domterm() else None 933 | 934 | def __call__(self, s1, s2=None, merge_into_prev=True, **kwargs): 935 | """ 936 | Adds to self.lines. 937 | This strange function signature is for historical reasons. 938 | 939 | :param str s1: 940 | :param str|None s2: 941 | :param bool merge_into_prev: if True and existing self.lines, merge into prev line. 942 | :param kwargs: passed to self.color 943 | """ 944 | if kwargs: 945 | s1 = self.color(s1, **kwargs) 946 | if s2 is not None: 947 | s1 = add_indent_lines(s1, s2) 948 | if merge_into_prev and self.lines: 949 | self.lines[-1] += s1 + "\n" 950 | else: 951 | self.lines.append(s1 + "\n") 952 | 953 | @contextlib.contextmanager 954 | def fold_text_ctx(self, line, merge_into_prev=True): 955 | """ 956 | Folds text, via :class:`DomTerm`, if available. 957 | Notes that this temporarily overwrites self.lines. 958 | 959 | :param str line: always visible 960 | :param bool merge_into_prev: if True and existing self.lines, merge into prev line. 961 | """ 962 | if not self.dom_term: 963 | self.__call__(line, merge_into_prev=merge_into_prev) 964 | yield 965 | return 966 | self.lines, old_lines = [], self.lines # overwrite self.lines 967 | yield # collect output (in new self.lines) 968 | self.lines, new_lines = old_lines, self.lines # recover self.lines 969 | hidden_text = "".join(new_lines) 970 | import io 971 | 972 | output_buf = io.StringIO() 973 | prefix = "" 974 | while line[:1] == " ": 975 | prefix += " " 976 | line = line[1:] 977 | self.dom_term.fold_text(line, hidden=hidden_text, file=output_buf, align=len(prefix)) 978 | output_text = prefix[1:] + output_buf.getvalue() 979 | if merge_into_prev and self.lines: 980 | self.lines[-1] += output_text 981 | else: 982 | self.lines.append(output_text) 983 | 984 | def _pp_extra_info(self, obj, depth_limit=3): 985 | """ 986 | :param typing.Any|typing.Sized obj: 987 | :param int depth_limit: 988 | :rtype: str 989 | """ 990 | # We want to exclude array types from Numpy, TensorFlow, PyTorch, etc. 991 | # Getting __getitem__ or __len__ on them even could lead to unexpected results or deadlocks, 992 | # depending on the context (e.g. inside a TF session run, extending the graph is unexpected). 993 | if hasattr(obj, "shape"): 994 | return "" 995 | s = [] 996 | if hasattr(obj, "__len__"): 997 | # noinspection PyBroadException 998 | try: 999 | if type(obj) in (str, unicode, list, tuple, dict) and len(obj) <= 5: 1000 | pass # don't print len in this case 1001 | else: 1002 | s += ["len = " + str(obj.__len__())] 1003 | except Exception: 1004 | pass 1005 | if depth_limit > 0 and hasattr(obj, "__getitem__"): 1006 | # noinspection PyBroadException 1007 | try: 1008 | if type(obj) in (str, unicode): 1009 | pass # doesn't make sense to get subitems here 1010 | else: 1011 | subobj = obj.__getitem__(0) # noqa 1012 | extra_info = self._pp_extra_info(subobj, depth_limit - 1) 1013 | if extra_info != "": 1014 | s += ["_[0]: {" + extra_info + "}"] 1015 | except Exception: 1016 | pass 1017 | return ", ".join(s) 1018 | 1019 | def pretty_print(self, obj): 1020 | """ 1021 | :param typing.Any obj: 1022 | :rtype: str 1023 | """ 1024 | s = repr(obj) 1025 | limit = output_limit() 1026 | if len(s) > limit: 1027 | if self.dom_term: 1028 | s = self.color.py_syntax_highlight(s) 1029 | s = self.dom_term.fold_text_string("", s) 1030 | else: 1031 | s = s[: limit - 3] # cut before syntax highlighting, to avoid missing color endings 1032 | s = self.color.py_syntax_highlight(s) 1033 | s += "..." 1034 | else: 1035 | s = self.color.py_syntax_highlight(s) 1036 | extra_info = self._pp_extra_info(obj) 1037 | if extra_info != "": 1038 | s += ", " + self.color.py_syntax_highlight(extra_info) 1039 | return s 1040 | 1041 | 1042 | # For compatibility, we keep non-PEP8 argument names. 1043 | # noinspection PyPep8Naming 1044 | def format_tb( 1045 | tb=None, 1046 | limit=None, 1047 | allLocals=None, 1048 | allGlobals=None, 1049 | withTitle=False, 1050 | with_color=None, 1051 | with_vars=None, 1052 | clear_frames=True, 1053 | ): 1054 | """ 1055 | :param types.TracebackType|types.FrameType|StackSummary tb: traceback. if None, will use sys._getframe 1056 | :param int|None limit: limit the traceback to this number of frames. by default, will look at sys.tracebacklimit 1057 | :param dict[str,typing.Any]|None allLocals: if set, will update it with all locals from all frames 1058 | :param dict[str,typing.Any]|None allGlobals: if set, will update it with all globals from all frames 1059 | :param bool withTitle: 1060 | :param bool|None with_color: output with ANSI escape codes for color 1061 | :param bool with_vars: will print var content which are referenced in the source code line. by default enabled. 1062 | :param bool clear_frames: whether to call frame.clear() after processing it. 1063 | That will potentially fix some mem leaks regarding locals, so it can be important. 1064 | Also see https://github.com/python/cpython/issues/113939. 1065 | However, any further access to frame locals will not work (e.g. if you want to use a debugger afterwards). 1066 | :return: list of strings (line-based) 1067 | :rtype: list[str] 1068 | """ 1069 | color = Color(enable=with_color) 1070 | output = _OutputLinesCollector(color=color) 1071 | 1072 | def format_filename(s): 1073 | """ 1074 | :param str s: 1075 | :rtype: str 1076 | """ 1077 | base = os.path.basename(s) 1078 | return ( 1079 | color('"' + s[: -len(base)], color.fg_colors[2]) 1080 | + color(base, color.fg_colors[2], bold=True) 1081 | + color('"', color.fg_colors[2]) 1082 | ) 1083 | 1084 | format_py_obj = output.pretty_print 1085 | if tb is None: 1086 | # noinspection PyBroadException 1087 | try: 1088 | tb = get_current_frame() 1089 | assert tb 1090 | except Exception: 1091 | output(color("format_tb: tb is None and sys._getframe() failed", color.fg_colors[1], bold=True)) 1092 | return output.lines 1093 | 1094 | def is_stack_summary(_tb): 1095 | """ 1096 | :param StackSummary|object _tb: 1097 | :rtype: bool 1098 | """ 1099 | return isinstance(_tb, StackSummary) 1100 | 1101 | isframe = inspect.isframe 1102 | if withTitle: 1103 | if isframe(tb) or is_stack_summary(tb): 1104 | output(color("Traceback (most recent call first):", color.fg_colors[0])) 1105 | else: # expect traceback-object (or compatible) 1106 | output(color("Traceback (most recent call last):", color.fg_colors[0])) 1107 | if with_vars is None and is_at_exit(): 1108 | # Better to not show __repr__ of some vars, as this might lead to crashes 1109 | # when native extensions are involved. 1110 | with_vars = False 1111 | if withTitle: 1112 | output("(Exclude vars because we are exiting.)") 1113 | if with_vars is None: 1114 | if any([f.f_code.co_name == "__del__" for f in iter_traceback()]): 1115 | # __del__ is usually called via the Python garbage collector (GC). 1116 | # This can happen and very random / non-deterministic places. 1117 | # There are cases where it is not safe to access some of the vars on the stack 1118 | # because they might be in a non-well-defined state, thus calling their __repr__ is not safe. 1119 | # See e.g. this bug: 1120 | # https://github.com/tensorflow/tensorflow/issues/22770 1121 | with_vars = False 1122 | if withTitle: 1123 | output("(Exclude vars because we are on a GC stack.)") 1124 | if with_vars is None: 1125 | with_vars = True 1126 | # noinspection PyBroadException 1127 | try: 1128 | if limit is None: 1129 | if hasattr(sys, "tracebacklimit"): 1130 | limit = sys.tracebacklimit 1131 | n = 0 1132 | _tb = tb 1133 | 1134 | class NotFound(Exception): 1135 | """ 1136 | Identifier not found. 1137 | """ 1138 | 1139 | def _resolve_identifier(namespace, keys): 1140 | """ 1141 | :param dict[str,typing.Any] namespace: 1142 | :param typing.Sequence[str] keys: 1143 | :return: namespace[name[0]][name[1]]... 1144 | """ 1145 | if keys[0] not in namespace: 1146 | raise NotFound() 1147 | obj = namespace[keys[0]] 1148 | for part in keys[1:]: 1149 | obj = getattr(obj, part) 1150 | return obj 1151 | 1152 | # noinspection PyShadowingNames 1153 | def _try_set(old, prefix, func): 1154 | """ 1155 | :param None|str old: 1156 | :param str prefix: 1157 | :param func: 1158 | :return: old 1159 | """ 1160 | if old is not None: 1161 | return old 1162 | try: 1163 | return add_indent_lines(prefix, func()) 1164 | except NotFound: 1165 | return old 1166 | except Exception as e: 1167 | return prefix + "!" + e.__class__.__name__ + ": " + str(e) 1168 | 1169 | while _tb is not None and (limit is None or n < limit): 1170 | if isframe(_tb): 1171 | f = _tb 1172 | elif is_stack_summary(_tb): 1173 | if isinstance(_tb[0], ExtendedFrameSummary): 1174 | f = _tb[0].tb_frame 1175 | else: 1176 | f = DummyFrame.from_frame_summary(_tb[0]) 1177 | else: 1178 | f = _tb.tb_frame 1179 | if allLocals is not None: 1180 | allLocals.update(f.f_locals) 1181 | if allGlobals is not None: 1182 | allGlobals.update(f.f_globals) 1183 | if hasattr(_tb, "tb_lineno"): 1184 | lineno = _tb.tb_lineno 1185 | elif is_stack_summary(_tb): 1186 | lineno = _tb[0].lineno 1187 | else: 1188 | lineno = f.f_lineno 1189 | co = f.f_code 1190 | filename = co.co_filename 1191 | if not os.path.isfile(filename): 1192 | alt_fn = fallback_findfile(filename) 1193 | if alt_fn: 1194 | filename = alt_fn 1195 | name = get_func_str_from_code_object(co, frame=f) 1196 | file_descr = "".join( 1197 | [ 1198 | " ", 1199 | color("File ", color.fg_colors[0], bold=True), 1200 | format_filename(filename), 1201 | ", ", 1202 | color("line ", color.fg_colors[0]), 1203 | color("%d" % lineno, color.fg_colors[4]), 1204 | ", ", 1205 | color("in ", color.fg_colors[0]), 1206 | name, 1207 | ] 1208 | ) 1209 | with output.fold_text_ctx(file_descr, merge_into_prev=False): 1210 | source_code = get_source_code(filename, lineno, f.f_globals) 1211 | if source_code: 1212 | source_code = remove_indent_lines(replace_tab_indents(source_code)).rstrip() 1213 | output(" line: ", color.py_syntax_highlight(source_code), color=color.fg_colors[0]) 1214 | if not with_vars: 1215 | pass 1216 | elif isinstance(f, DummyFrame) and not f.have_vars_available: 1217 | pass 1218 | else: 1219 | with output.fold_text_ctx(color(" locals:", color.fg_colors[0])): 1220 | already_printed_locals = set() # type: typing.Set[typing.Tuple[str,...]] 1221 | for token_str in grep_full_py_identifiers(parse_py_statement(source_code)): 1222 | splitted_token = tuple(token_str.split(".")) 1223 | for token in [splitted_token[0:i] for i in range(1, len(splitted_token) + 1)]: 1224 | if token in already_printed_locals: 1225 | continue 1226 | token_value = None 1227 | token_value = _try_set( 1228 | token_value, 1229 | color(" ", color.fg_colors[0]), 1230 | lambda: format_py_obj(_resolve_identifier(f.f_locals, token)), 1231 | ) 1232 | token_value = _try_set( 1233 | token_value, 1234 | color(" ", color.fg_colors[0]), 1235 | lambda: format_py_obj(_resolve_identifier(f.f_globals, token)), 1236 | ) 1237 | token_value = _try_set( 1238 | token_value, 1239 | color(" ", color.fg_colors[0]), 1240 | lambda: format_py_obj(_resolve_identifier(f.f_builtins, token)), 1241 | ) 1242 | token_value = token_value or color("", color.fg_colors[0]) 1243 | prefix = " %s " % color(".", color.fg_colors[0], bold=True).join( 1244 | token 1245 | ) + color("= ", color.fg_colors[0], bold=True) 1246 | output(prefix, token_value) 1247 | already_printed_locals.add(token) 1248 | if len(already_printed_locals) == 0: 1249 | output(color(" no locals", color.fg_colors[0])) 1250 | else: 1251 | output(color(" -- code not available --", color.fg_colors[0])) 1252 | 1253 | if clear_frames: 1254 | # Just like :func:`traceback.clear_frames`, but has an additional fix 1255 | # (https://github.com/python/cpython/issues/113939). 1256 | try: 1257 | f.clear() 1258 | except RuntimeError: 1259 | pass 1260 | else: 1261 | # Using this code triggers that the ref actually goes out of scope, otherwise it does not! 1262 | # https://github.com/python/cpython/issues/113939 1263 | f.f_locals # noqa 1264 | 1265 | if isframe(_tb): 1266 | _tb = _tb.f_back 1267 | elif is_stack_summary(_tb): 1268 | _tb = StackSummary.from_list(_tb[1:]) 1269 | if not _tb: 1270 | _tb = None 1271 | else: 1272 | _tb = _tb.tb_next 1273 | n += 1 1274 | 1275 | except Exception: 1276 | output(color("ERROR: cannot get more detailed exception info because:", color.fg_colors[1], bold=True)) 1277 | import traceback 1278 | 1279 | for line in traceback.format_exc().split("\n"): 1280 | output(" " + line) 1281 | 1282 | return output.lines 1283 | 1284 | 1285 | def print_tb(tb, file=None, **kwargs): 1286 | """ 1287 | Replacement for traceback.print_tb. 1288 | 1289 | :param types.TracebackType|types.FrameType|StackSummary tb: 1290 | :param io.TextIOBase|io.StringIO|typing.TextIO|None file: stderr by default 1291 | :return: nothing, prints to ``file`` 1292 | """ 1293 | if file is None: 1294 | file = sys.stderr 1295 | for line in format_tb(tb=tb, **kwargs): 1296 | file.write(line) 1297 | file.flush() 1298 | 1299 | 1300 | def print_exception(etype, value, tb, limit=None, file=None, chain=True): 1301 | """ 1302 | Replacement for traceback.print_exception. 1303 | 1304 | :param etype: exception type 1305 | :param value: exception value 1306 | :param tb: traceback 1307 | :param int|None limit: 1308 | :param io.TextIOBase|io.StringIO|typing.TextIO|None file: stderr by default 1309 | :param bool chain: whether to print the chain of exceptions 1310 | """ 1311 | better_exchook(etype, value, tb, autodebugshell=False, file=file, limit=limit, chain=chain) 1312 | 1313 | 1314 | def print_exc(limit=None, file=None, chain=True): 1315 | """ 1316 | Replacement for traceback.print_exc. 1317 | Shorthand for 'print_exception(*sys.exc_info(), limit, file)'. 1318 | 1319 | :param int|None limit: 1320 | :param io.TextIOBase|io.StringIO|typing.TextIO|None file: stderr by default 1321 | :param bool chain: 1322 | """ 1323 | print_exception(*sys.exc_info(), limit=limit, file=file, chain=chain) 1324 | 1325 | 1326 | def better_exchook( 1327 | etype, 1328 | value, 1329 | tb, 1330 | debugshell=False, 1331 | autodebugshell=True, 1332 | file=None, 1333 | with_color=None, 1334 | with_preamble=True, 1335 | limit=None, 1336 | chain=True, 1337 | ): 1338 | """ 1339 | Replacement for sys.excepthook. 1340 | 1341 | :param etype: exception type 1342 | :param value: exception value 1343 | :param tb: traceback 1344 | :param bool debugshell: spawn a debug shell at the context of the exception 1345 | :param bool autodebugshell: if env DEBUG is an integer != 0, it will spawn a debug shell 1346 | :param io.TextIOBase|io.StringIO|typing.TextIO|None file: output stream where we will print the traceback 1347 | and exception information. stderr by default. 1348 | :param bool|None with_color: whether to use ANSI escape codes for colored output 1349 | :param bool with_preamble: print a short preamble for the exception 1350 | :param int|None limit: 1351 | :param bool chain: whether to print the chain of exceptions 1352 | """ 1353 | if file is None: 1354 | file = sys.stderr 1355 | 1356 | if autodebugshell: 1357 | # noinspection PyBroadException 1358 | try: 1359 | debugshell = int(os.environ["DEBUG"]) != 0 1360 | except Exception: 1361 | pass 1362 | 1363 | color = Color(enable=with_color) 1364 | output = _OutputLinesCollector(color=color) 1365 | 1366 | rec_args = dict(autodebugshell=False, file=file, with_color=with_color, with_preamble=with_preamble) 1367 | if chain: 1368 | if getattr(value, "__cause__", None): 1369 | better_exchook(type(value.__cause__), value.__cause__, value.__cause__.__traceback__, **rec_args) 1370 | output("") 1371 | output("The above exception was the direct cause of the following exception:") 1372 | output("") 1373 | elif getattr(value, "__context__", None): 1374 | better_exchook(type(value.__context__), value.__context__, value.__context__.__traceback__, **rec_args) 1375 | output("") 1376 | output("During handling of the above exception, another exception occurred:") 1377 | output("") 1378 | 1379 | def format_filename(s): 1380 | """ 1381 | :param str s: 1382 | :rtype: str 1383 | """ 1384 | base = os.path.basename(s) 1385 | return ( 1386 | color('"' + s[: -len(base)], color.fg_colors[2]) 1387 | + color(base, color.fg_colors[2], bold=True) 1388 | + color('"', color.fg_colors[2]) 1389 | ) 1390 | 1391 | if with_preamble: 1392 | output(color("EXCEPTION", color.fg_colors[1], bold=True)) 1393 | all_locals, all_globals = {}, {} 1394 | if tb is not None: 1395 | output.lines.extend( 1396 | format_tb( 1397 | tb=tb, 1398 | limit=limit, 1399 | allLocals=all_locals, 1400 | allGlobals=all_globals, 1401 | withTitle=True, 1402 | with_color=color.enable, 1403 | clear_frames=not debugshell, 1404 | ) 1405 | ) 1406 | else: 1407 | output(color("better_exchook: traceback unknown", color.fg_colors[1])) 1408 | 1409 | if isinstance(value, SyntaxError): 1410 | # The standard except hook will also print the source of the SyntaxError, 1411 | # so do it in a similar way here as well. 1412 | filename = value.filename 1413 | # Keep the output somewhat consistent with format_tb. 1414 | file_descr = "".join( 1415 | [ 1416 | " ", 1417 | color("File ", color.fg_colors[0], bold=True), 1418 | format_filename(filename), 1419 | ", ", 1420 | color("line ", color.fg_colors[0]), 1421 | color("%d" % value.lineno, color.fg_colors[4]), 1422 | ] 1423 | ) 1424 | with output.fold_text_ctx(file_descr): 1425 | if not os.path.isfile(filename): 1426 | alt_fn = fallback_findfile(filename) 1427 | if alt_fn: 1428 | output( 1429 | color(" -- couldn't find file, trying this instead: ", color.fg_colors[0]) 1430 | + format_filename(alt_fn) 1431 | ) 1432 | filename = alt_fn 1433 | source_code = get_source_code(filename, value.lineno) 1434 | if source_code: 1435 | # Similar to remove_indent_lines. 1436 | # But we need to know the indent-prefix such that we can use the syntax-error offset. 1437 | source_code = replace_tab_indents(source_code) 1438 | lines = source_code.splitlines(True) 1439 | indent_prefix = get_same_indent_prefix(lines) 1440 | if indent_prefix is None: 1441 | indent_prefix = "" 1442 | source_code = "".join([line[len(indent_prefix) :] for line in lines]) 1443 | source_code = source_code.rstrip() 1444 | prefix = " line: " 1445 | output(prefix, color.py_syntax_highlight(source_code), color=color.fg_colors[0]) 1446 | output(" " * (len(prefix) + value.offset - len(indent_prefix) - 1) + "^", color=color.fg_colors[4]) 1447 | 1448 | import types 1449 | 1450 | # noinspection PyShadowingNames 1451 | def _some_str(value): 1452 | """ 1453 | :param object value: 1454 | :rtype: str 1455 | """ 1456 | # noinspection PyBroadException 1457 | try: 1458 | return str(value) 1459 | except Exception: 1460 | return "" % type(value).__name__ 1461 | 1462 | # noinspection PyShadowingNames 1463 | def _format_final_exc_line(etype, value): 1464 | value_str = _some_str(value) 1465 | if value is None or not value_str: 1466 | line = color("%s" % etype, color.fg_colors[1]) 1467 | else: 1468 | line = color("%s" % etype, color.fg_colors[1]) + ": %s" % (value_str,) 1469 | return line 1470 | 1471 | # noinspection PyUnresolvedReferences 1472 | if ( 1473 | isinstance(etype, BaseException) 1474 | or (hasattr(types, "InstanceType") and isinstance(etype, types.InstanceType)) 1475 | or etype is None 1476 | or type(etype) is str 1477 | ): 1478 | output(_format_final_exc_line(etype, value)) 1479 | else: 1480 | output(_format_final_exc_line(etype.__name__, value)) 1481 | 1482 | for line in output.lines: 1483 | file.write(line) 1484 | file.flush() 1485 | 1486 | if debugshell: 1487 | file.write("---------- DEBUG SHELL -----------\n") 1488 | file.flush() 1489 | debug_shell(user_ns=all_locals, user_global_ns=all_globals, traceback=tb) 1490 | 1491 | 1492 | def dump_all_thread_tracebacks(exclude_thread_ids=None, file=None): 1493 | """ 1494 | Prints the traceback of all threads. 1495 | 1496 | :param set[int]|list[int]|None exclude_thread_ids: threads to exclude 1497 | :param io.TextIOBase|io.StringIO|typing.TextIO|None file: output stream 1498 | """ 1499 | if exclude_thread_ids is None: 1500 | exclude_thread_ids = [] 1501 | if not file: 1502 | file = sys.stdout 1503 | import threading 1504 | 1505 | if hasattr(sys, "_current_frames"): 1506 | print("", file=file) 1507 | threads = {t.ident: t for t in threading.enumerate()} 1508 | # noinspection PyProtectedMember 1509 | for tid, stack in sys._current_frames().items(): 1510 | if tid in exclude_thread_ids: 1511 | continue 1512 | # This is a bug in earlier Python versions. 1513 | # https://bugs.python.org/issue17094 1514 | # Note that this leaves out all threads not created via the threading module. 1515 | if tid not in threads: 1516 | continue 1517 | tags = [] 1518 | thread = threads.get(tid) 1519 | if thread: 1520 | assert isinstance(thread, threading.Thread) 1521 | if thread is threading.current_thread(): 1522 | tags += ["current"] 1523 | # noinspection PyProtectedMember,PyUnresolvedReferences 1524 | if isinstance(thread, threading._MainThread): 1525 | tags += ["main"] 1526 | tags += [str(thread)] 1527 | else: 1528 | tags += ["unknown with id %i" % tid] 1529 | print("Thread %s:" % ", ".join(tags), file=file) 1530 | print_tb(stack, file=file) 1531 | print("", file=file) 1532 | print("That were all threads.", file=file) 1533 | else: 1534 | print("Does not have sys._current_frames, cannot get thread tracebacks.", file=file) 1535 | 1536 | 1537 | def get_current_frame(): 1538 | """ 1539 | :return: current frame object (excluding this function call) 1540 | :rtype: types.FrameType 1541 | 1542 | Uses sys._getframe if available, otherwise some trickery with sys.exc_info and a dummy exception. 1543 | """ 1544 | if hasattr(sys, "_getframe"): 1545 | # noinspection PyProtectedMember 1546 | return sys._getframe(1) 1547 | try: 1548 | raise ZeroDivisionError 1549 | except ZeroDivisionError: 1550 | return sys.exc_info()[2].tb_frame.f_back 1551 | 1552 | 1553 | def get_func_str_from_code_object(co, frame=None): 1554 | """ 1555 | :param types.CodeType co: 1556 | :param types.FrameType|DummyFrame|None frame: if given, might provide a faster way to get the function name 1557 | :return: co.co_name as fallback, but maybe sth better like the full func name if possible 1558 | :rtype: str 1559 | """ 1560 | f = get_func_from_code_object(co, frame=frame) 1561 | if f: 1562 | if hasattr(f, "__qualname__"): 1563 | return f.__qualname__ 1564 | return str(f) 1565 | return co.co_name 1566 | 1567 | 1568 | _func_from_code_object_cache = WeakKeyDictionary() # code object -> function 1569 | 1570 | 1571 | def get_func_from_code_object(co, frame=None): 1572 | """ 1573 | :param types.CodeType co: 1574 | :param types.FrameType|DummyFrame|None frame: if given, might provide a faster way to get the function name 1575 | :return: function, such that ``func.__code__ is co``, or None 1576 | :rtype: types.FunctionType 1577 | 1578 | This is CPython specific (to some degree; it uses the `gc` module to find references). 1579 | Inspired from: 1580 | https://stackoverflow.com/questions/12787108/getting-the-python-function-for-a-code-object 1581 | https://stackoverflow.com/questions/54656758/get-function-object-from-stack-frame-object 1582 | """ 1583 | import gc 1584 | import types 1585 | 1586 | assert isinstance(co, (types.CodeType, DummyFrame)) 1587 | co_is_code_object = isinstance(co, types.CodeType) 1588 | if co_is_code_object: 1589 | candidate = _func_from_code_object_cache.get(co) 1590 | if candidate: 1591 | return candidate 1592 | _attr_name = "__code__" if PY3 else "func_code" 1593 | if frame and frame.f_code.co_nlocals > 0: 1594 | func_name = frame.f_code.co_name 1595 | frame_self = frame.f_locals.get("self") 1596 | if frame_self is not None: 1597 | candidate = getattr(frame_self.__class__, func_name, None) 1598 | if candidate and (getattr(candidate, _attr_name, None) is co or isinstance(co, DummyFrame)): 1599 | if co_is_code_object: 1600 | _func_from_code_object_cache[co] = candidate 1601 | return candidate 1602 | try: 1603 | candidate = getattr(_get_loaded_module_from_filename(co.co_filename), co.co_name, None) 1604 | except ImportError: # some modules have lazy loaders, but those might fail here 1605 | candidate = None 1606 | if candidate and (getattr(candidate, _attr_name, None) is co or isinstance(co, DummyFrame)): 1607 | if co_is_code_object: 1608 | _func_from_code_object_cache[co] = candidate 1609 | return candidate 1610 | if isinstance(co, DummyFrame): 1611 | return None 1612 | candidates = gc.get_referrers(co) 1613 | candidates = [f for f in candidates if getattr(f, _attr_name, None) is co] 1614 | if candidates: 1615 | _func_from_code_object_cache[co] = candidates[0] 1616 | return candidates[0] 1617 | return None 1618 | 1619 | 1620 | _loaded_module_from_filename_cache = {} # filename -> module name 1621 | 1622 | 1623 | def _get_loaded_module_from_filename(filename): 1624 | """ 1625 | Like inspect.getmodule but faster. 1626 | 1627 | :param str filename: 1628 | :rtype: types.ModuleType|Any|None 1629 | """ 1630 | if filename.endswith(".pyc") or filename.endswith(".pyo"): 1631 | filename = filename[:-1] 1632 | if filename in _loaded_module_from_filename_cache: 1633 | return sys.modules.get(_loaded_module_from_filename_cache[filename]) 1634 | # Update the filename to module name cache and check yet again 1635 | # Copy sys.modules in order to cope with changes while iterating 1636 | for modname, module in sys.modules.copy().items(): 1637 | f = getattr(module, "__file__", None) 1638 | if f: 1639 | if f.endswith(".pyc") or f.endswith(".pyo"): 1640 | f = f[:-1] 1641 | _loaded_module_from_filename_cache[f] = modname 1642 | if filename in _loaded_module_from_filename_cache: 1643 | return sys.modules.get(_loaded_module_from_filename_cache[filename]) 1644 | return None 1645 | 1646 | 1647 | def iter_traceback(tb=None, enforce_most_recent_call_first=False): 1648 | """ 1649 | Iterates a traceback of various formats: 1650 | - traceback (types.TracebackType) 1651 | - frame object (types.FrameType) 1652 | - stack summary (traceback.StackSummary) 1653 | 1654 | :param types.TracebackType|types.FrameType|StackSummary|None tb: traceback. if None, will use sys._getframe 1655 | :param bool enforce_most_recent_call_first: 1656 | Frame or stack summery: most recent call first (top of the stack is the first entry in the result) 1657 | Traceback: most recent call last 1658 | If True, and we get traceback, will unroll and reverse, such that we have always the most recent call first. 1659 | :return: yields the frames (types.FrameType) 1660 | :rtype: list[types.FrameType|DummyFrame] 1661 | """ 1662 | if tb is None: 1663 | tb = get_current_frame() 1664 | 1665 | def is_stack_summary(_tb): 1666 | """ 1667 | :param StackSummary|object _tb: 1668 | :rtype: bool 1669 | """ 1670 | return isinstance(_tb, StackSummary) 1671 | 1672 | is_frame = inspect.isframe 1673 | is_traceback = inspect.istraceback 1674 | assert is_traceback(tb) or is_frame(tb) or is_stack_summary(tb) 1675 | # Frame or stack summery: most recent call first 1676 | # Traceback: most recent call last 1677 | if is_traceback(tb) and enforce_most_recent_call_first: 1678 | frames = list(iter_traceback(tb)) 1679 | for frame in frames[::-1]: 1680 | yield frame 1681 | return 1682 | 1683 | _tb = tb 1684 | while _tb is not None: 1685 | if is_frame(_tb): 1686 | frame = _tb 1687 | elif is_stack_summary(_tb): 1688 | if isinstance(_tb[0], ExtendedFrameSummary): 1689 | frame = _tb[0].tb_frame 1690 | else: 1691 | frame = DummyFrame.from_frame_summary(_tb[0]) 1692 | else: 1693 | frame = _tb.tb_frame 1694 | yield frame 1695 | if is_frame(_tb): 1696 | _tb = _tb.f_back 1697 | elif is_stack_summary(_tb): 1698 | _tb = StackSummary.from_list(_tb[1:]) 1699 | if not _tb: 1700 | _tb = None 1701 | else: 1702 | _tb = _tb.tb_next 1703 | 1704 | 1705 | class ExtendedFrameSummary(FrameSummary): 1706 | """ 1707 | Extends :class:`FrameSummary` by ``self.tb_frame``. 1708 | """ 1709 | 1710 | def __init__(self, frame, **kwargs): 1711 | super(ExtendedFrameSummary, self).__init__(**kwargs) 1712 | self.tb_frame = frame 1713 | 1714 | def __reduce__(self): 1715 | # We deliberately serialize this as just a FrameSummary, 1716 | # excluding the tb_frame, as this cannot be serialized (via pickle at least), 1717 | # and also we do not want this to be serialized. 1718 | # We also exclude _line and locals as it's not so easy to add them here, 1719 | # and also not so important. 1720 | return FrameSummary, (self.filename, self.lineno, self.name) 1721 | 1722 | 1723 | class DummyFrame: 1724 | """ 1725 | This class has the same attributes as a code and a frame object 1726 | and is intended to be used as a dummy replacement. 1727 | """ 1728 | 1729 | @classmethod 1730 | def from_frame_summary(cls, f): 1731 | """ 1732 | :param FrameSummary f: 1733 | :rtype: DummyFrame 1734 | """ 1735 | return cls(filename=f.filename, lineno=f.lineno, name=f.name, f_locals=f.locals) 1736 | 1737 | def __init__(self, filename, lineno, name, f_locals=None, f_globals=None, f_builtins=None): 1738 | self.lineno = lineno 1739 | self.tb_lineno = lineno 1740 | self.f_lineno = lineno 1741 | self.f_code = self 1742 | self.filename = filename 1743 | self.co_filename = filename 1744 | self.name = name 1745 | self.co_name = name 1746 | self.f_locals = f_locals or {} 1747 | self.f_globals = f_globals or {} 1748 | self.f_builtins = f_builtins or {} 1749 | self.have_vars_available = f_locals is not None or f_globals is not None or f_builtins is not None 1750 | self.co_nlocals = len(self.f_locals) if self.f_locals is not self.f_globals else 0 1751 | 1752 | def clear(self): 1753 | """clear""" 1754 | self.f_locals = None 1755 | 1756 | 1757 | # noinspection PyPep8Naming,PyUnusedLocal 1758 | def _StackSummary_extract(frame_gen, limit=None, lookup_lines=True, capture_locals=False): 1759 | """ 1760 | Replacement for :func:`StackSummary.extract`. 1761 | 1762 | Create a StackSummary from a traceback or stack object. 1763 | Very simplified copy of the original StackSummary.extract(). 1764 | We want always to capture locals, that is why we overwrite it. 1765 | Additionally, we also capture the frame. 1766 | This is a bit hacky and also not like this is originally intended (to not keep refs). 1767 | 1768 | :param frame_gen: A generator that yields (frame, lineno) tuples to 1769 | include in the stack. 1770 | :param limit: None to include all frames or the number of frames to 1771 | include. 1772 | :param lookup_lines: If True, lookup lines for each frame immediately, 1773 | otherwise lookup is deferred until the frame is rendered. 1774 | :param capture_locals: If True, the local variables from each frame will 1775 | be captured as object representations into the FrameSummary. 1776 | """ 1777 | result = StackSummary() 1778 | for f, lineno in frame_gen: 1779 | co = f.f_code 1780 | filename = co.co_filename 1781 | name = co.co_name 1782 | result.append(ExtendedFrameSummary(frame=f, filename=filename, lineno=lineno, name=name, lookup_line=False)) 1783 | return result 1784 | 1785 | 1786 | def install(): 1787 | """ 1788 | Replaces sys.excepthook by our better_exchook. 1789 | """ 1790 | sys.excepthook = better_exchook 1791 | 1792 | 1793 | def replace_traceback_format_tb(): 1794 | """ 1795 | Replaces these functions from the traceback module by our own: 1796 | 1797 | - traceback.format_tb 1798 | - traceback.StackSummary.format 1799 | - traceback.StackSummary.extract 1800 | 1801 | Note that this kind of monkey patching might not be safe under all circumstances 1802 | and is not officially supported by Python. 1803 | """ 1804 | import traceback 1805 | 1806 | traceback.format_tb = format_tb 1807 | if hasattr(traceback, "StackSummary"): 1808 | traceback.StackSummary.format = format_tb 1809 | traceback.StackSummary.extract = _StackSummary_extract 1810 | 1811 | 1812 | def replace_traceback_print_tb(): 1813 | """ 1814 | Replaces these functions from the traceback module by our own: 1815 | 1816 | - traceback.print_tb 1817 | - traceback.print_exception 1818 | - traceback.print_exc 1819 | 1820 | Note that this kind of monkey patching might not be safe under all circumstances 1821 | and is not officially supported by Python. 1822 | """ 1823 | import traceback 1824 | 1825 | traceback.print_tb = print_tb 1826 | traceback.print_exception = print_exception 1827 | traceback.print_exc = print_exc 1828 | 1829 | 1830 | def setup_all(): 1831 | """ 1832 | Calls: 1833 | 1834 | - :func:`install` 1835 | - :func:`replace_traceback_format_tb` 1836 | - :func:`replace_traceback_print_tb` 1837 | """ 1838 | install() 1839 | replace_traceback_format_tb() 1840 | replace_traceback_print_tb() 1841 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from argparse import ArgumentParser 3 | from better_exchook import better_exchook, install, debug_shell 4 | 5 | 6 | # noinspection PyMissingOrEmptyDocstring,PyBroadException 7 | def demo(): 8 | """ 9 | Some demo. 10 | """ 11 | # some examples 12 | # this code produces this output: https://gist.github.com/922622 13 | 14 | try: 15 | x = {1: 2, "a": "b"} 16 | 17 | # noinspection PyMissingOrEmptyDocstring 18 | def f(): 19 | y = "foo" 20 | # noinspection PyUnresolvedReferences,PyStatementEffect 21 | x, 42, sys.stdin.__class__, sys.exc_info, y, z 22 | 23 | f() 24 | except Exception: 25 | better_exchook(*sys.exc_info()) 26 | 27 | try: 28 | # noinspection PyArgumentList 29 | (lambda _x: None)(__name__, 42) # multiline 30 | except Exception: 31 | better_exchook(*sys.exc_info()) 32 | 33 | try: 34 | 35 | class Obj: 36 | def __repr__(self): 37 | return "" 38 | 39 | obj = Obj() 40 | assert not obj 41 | except Exception: 42 | better_exchook(*sys.exc_info()) 43 | 44 | # noinspection PyMissingOrEmptyDocstring 45 | def f1(a): 46 | f2(a + 1, 2) 47 | 48 | # noinspection PyMissingOrEmptyDocstring 49 | def f2(a, b): 50 | f3(a + b) 51 | 52 | # noinspection PyMissingOrEmptyDocstring 53 | def f3(a): 54 | b = ("abc" * 100) + "-interesting" # some long demo str 55 | a(b) # error, not callable 56 | 57 | try: 58 | f1(13) 59 | except Exception: 60 | better_exchook(*sys.exc_info()) 61 | 62 | # use this to overwrite the global exception handler 63 | install() 64 | # and fail 65 | # noinspection PyUnresolvedReferences 66 | finalfail(sys) 67 | 68 | 69 | def _debug_shell(): 70 | debug_shell(locals(), globals()) 71 | 72 | 73 | def _debug_shell_exception(): 74 | # noinspection PyBroadException 75 | try: 76 | raise Exception("demo exception") 77 | except Exception: 78 | better_exchook(*sys.exc_info(), debugshell=True) 79 | 80 | 81 | def main(): 82 | arg_parser = ArgumentParser() 83 | arg_parser.add_argument("command", help="demo, debug_shell, ...", nargs="?") 84 | args = arg_parser.parse_args() 85 | if args.command: 86 | if "_%s" % args.command in globals(): 87 | func_name = "_%s" % args.command 88 | elif args.command in globals(): 89 | func_name = args.command 90 | else: 91 | print("Error: Function (_)%s not found." % args.command) 92 | sys.exit(1) 93 | print("Run %s()." % func_name) 94 | func = globals()[func_name] 95 | func() 96 | sys.exit() 97 | 98 | # Just run the demo. 99 | demo() 100 | 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | target-version = ["py37"] 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage: 3 | 4 | Create ~/.pypirc with info: 5 | 6 | [distutils] 7 | index-servers = 8 | pypi 9 | 10 | [pypi] 11 | repository: https://upload.pypi.org/legacy/ 12 | username: ... 13 | password: ... 14 | 15 | (Not needed anymore) Registering the project: python3 setup.py register 16 | New release: python3 setup.py sdist upload 17 | 18 | I had some trouble at some point, and this helped: 19 | pip3 install --user twine 20 | python3 setup.py sdist 21 | twine upload dist/*.tar.gz 22 | 23 | See also MANIFEST.in for included files. 24 | 25 | For debugging this script: 26 | 27 | python3 setup.py sdist 28 | pip3 install --user dist/*.tar.gz -v 29 | (Without -v, all stdout/stderr from here will not be shown.) 30 | 31 | """ 32 | 33 | from distutils.core import setup 34 | import time 35 | from pprint import pprint 36 | import os 37 | import sys 38 | from subprocess import Popen, check_output, PIPE 39 | 40 | 41 | def debug_print_file(fn): 42 | print("%s:" % fn) 43 | if not os.path.exists(fn): 44 | print("") 45 | return 46 | if os.path.isdir(fn): 47 | print("") 48 | pprint(os.listdir(fn)) 49 | return 50 | print(open(fn).read()) 51 | 52 | 53 | def parse_pkg_info(fn): 54 | """ 55 | :param str fn: 56 | :rtype: dict[str,str] 57 | """ 58 | res = {} 59 | for ln in open(fn).read().splitlines(): 60 | if not ln or not ln[:1].strip(): 61 | continue 62 | if ": " not in ln: 63 | continue 64 | key, value = ln.split(": ", 1) 65 | res[key] = value 66 | return res 67 | 68 | 69 | def git_commit_rev(commit="HEAD", git_dir="."): 70 | if commit is None: 71 | commit = "HEAD" 72 | return check_output(["git", "rev-parse", "--short", commit], cwd=git_dir).decode("utf8").strip() 73 | 74 | 75 | def git_is_dirty(git_dir="."): 76 | proc = Popen(["git", "diff", "--no-ext-diff", "--quiet", "--exit-code"], cwd=git_dir, stdout=PIPE) 77 | proc.communicate() 78 | if proc.returncode == 0: 79 | return False 80 | if proc.returncode == 1: 81 | return True 82 | raise Exception("unexpected return code %i" % proc.returncode) 83 | 84 | 85 | def git_commit_date(commit="HEAD", git_dir="."): 86 | out = check_output(["git", "show", "-s", "--format=%ci", commit], cwd=git_dir).decode("utf8") 87 | out = out.strip()[:-6].replace(":", "").replace("-", "").replace(" ", ".") 88 | return out 89 | 90 | 91 | def git_head_version(git_dir="."): 92 | commit_date = git_commit_date(git_dir=git_dir) # like "20190202.154527" 93 | # rev = git_commit_rev(git_dir=git_dir) 94 | # is_dirty = git_is_dirty(git_dir=git_dir) 95 | # Make this distutils.version.StrictVersion compatible. 96 | return "1.%s" % commit_date 97 | 98 | 99 | if os.path.exists("PKG-INFO"): 100 | print("Found existing PKG-INFO.") 101 | info = parse_pkg_info("PKG-INFO") 102 | version = info["Version"] 103 | print("Version via PKG-INFO:", version) 104 | else: 105 | try: 106 | version = git_head_version() 107 | print("Version via Git:", version) 108 | except Exception as exc: 109 | print("Exception while getting Git version:", exc) 110 | sys.excepthook(*sys.exc_info()) 111 | version = time.strftime("1.%Y%m%d.%H%M%S", time.gmtime()) 112 | print("Version via current time:", version) 113 | 114 | 115 | if os.environ.get("DEBUG", "") == "1": 116 | debug_print_file(".") 117 | debug_print_file("PKG-INFO") 118 | 119 | 120 | setup( 121 | name="better_exchook", 122 | version=version, 123 | packages=["better_exchook"], 124 | package_dir={"better_exchook": ""}, 125 | description="nice Python exception hook replacement", 126 | author="Albert Zeyer", 127 | author_email="albzey@gmail.com", 128 | url="https://github.com/albertz/py_better_exchook", 129 | license="2-clause BSD license", 130 | long_description=open("README.rst").read(), 131 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 132 | classifiers=[ 133 | "Development Status :: 5 - Production/Stable", 134 | "Intended Audience :: Developers", 135 | "Intended Audience :: Education", 136 | "Intended Audience :: Science/Research", 137 | "License :: OSI Approved :: BSD License", 138 | "Operating System :: MacOS :: MacOS X", 139 | "Operating System :: Microsoft :: Windows", 140 | "Operating System :: POSIX", 141 | "Operating System :: Unix", 142 | "Programming Language :: Python", 143 | "Topic :: Software Development :: Libraries :: Python Modules", 144 | ], 145 | ) 146 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | import tempfile 3 | from better_exchook import * 4 | import sys 5 | 6 | PY2 = sys.version_info[0] == 2 7 | 8 | if PY2: 9 | from io import BytesIO as StringIO 10 | else: 11 | from io import StringIO 12 | 13 | _IsGithubEnv = os.environ.get("GITHUB_ACTIONS") == "true" 14 | 15 | 16 | def _fold_open(txt): 17 | if _IsGithubEnv: 18 | print("::group::%s" % txt) 19 | 20 | 21 | def _fold_close(): 22 | if _IsGithubEnv: 23 | print("::endgroup::") 24 | 25 | 26 | def test_is_source_code_missing_open_brackets(): 27 | """ 28 | Test :func:`is_source_code_missing_open_brackets`. 29 | """ 30 | assert is_source_code_missing_open_brackets("a") is False 31 | assert is_source_code_missing_open_brackets("a)") is True 32 | assert is_source_code_missing_open_brackets("fn()") is False 33 | assert is_source_code_missing_open_brackets("fn().b()") is False 34 | assert is_source_code_missing_open_brackets("fn().b()[0]") is False 35 | assert is_source_code_missing_open_brackets("fn({a[0]: 'b'}).b()[0]") is False 36 | assert is_source_code_missing_open_brackets("a[0]: 'b'}).b()[0]") is True 37 | 38 | 39 | def test_add_indent_lines(): 40 | """ 41 | Test :func:`add_indent_lines`. 42 | """ 43 | assert add_indent_lines("foo ", " bar") == "foo bar" 44 | assert add_indent_lines("foo ", " bar\n baz") == "foo bar\n baz" 45 | 46 | 47 | def test_get_same_indent_prefix(): 48 | """ 49 | Test :func:`get_same_indent_prefix`. 50 | """ 51 | assert get_same_indent_prefix(["a", "b"]) == "" 52 | assert get_same_indent_prefix([" a"]) == " " 53 | assert get_same_indent_prefix([" a", " b"]) == " " 54 | 55 | 56 | def test_remove_indent_lines(): 57 | """ 58 | Test :func:`remove_indent_lines`. 59 | """ 60 | assert remove_indent_lines(" a\n b") == "a\n b" 61 | assert remove_indent_lines(" a\n b") == "a\nb" 62 | assert remove_indent_lines("\ta\n\t b") == "a\n b" 63 | 64 | 65 | def _import_dummy_mod_by_path(filename): 66 | """ 67 | :param str filename: 68 | """ 69 | dummy_mod_name = "_dummy_mod_name" 70 | if sys.version_info[0] == 2: 71 | # noinspection PyDeprecation 72 | import imp 73 | 74 | # noinspection PyDeprecation 75 | imp.load_source(dummy_mod_name, filename) 76 | else: 77 | import importlib.util 78 | 79 | spec = importlib.util.spec_from_file_location(dummy_mod_name, filename) 80 | mod = importlib.util.module_from_spec(spec) 81 | spec.loader.exec_module(mod) # noqa 82 | 83 | 84 | def _run_code_format_exc(txt, expected_exception, except_hook=better_exchook): 85 | """ 86 | :param str txt: 87 | :param type[Exception] expected_exception: exception class 88 | :return: stdout of better_exchook 89 | :rtype: str 90 | """ 91 | exc_stdout = StringIO() 92 | with tempfile.NamedTemporaryFile(mode="w", suffix=".py") as f: 93 | f.write(txt) 94 | f.flush() 95 | filename = f.name 96 | try: 97 | _import_dummy_mod_by_path(filename) 98 | except expected_exception: 99 | except_hook(*sys.exc_info(), file=exc_stdout) 100 | except Exception: # some other exception 101 | # Note: Any exception which falls through would miss the source code 102 | # in the exception handler output because the file got deleted. 103 | # So handle it now. 104 | sys.excepthook(*sys.exc_info()) 105 | print("-" * 40) 106 | raise Exception("Got unexpected exception.") 107 | else: 108 | raise Exception("We expected to get a %s..." % expected_exception.__name__) 109 | return exc_stdout.getvalue() 110 | 111 | 112 | def test_syntax_error(): 113 | """ 114 | Test :class:`SyntaxError`. 115 | """ 116 | exc_stdout = _run_code_format_exc("[\n\ndef foo():\n pass\n", expected_exception=SyntaxError) 117 | # The standard exception hook prints sth like this: 118 | """ 119 | File "/var/tmp/tmpx9twr8i2.py", line 3 120 | def foo(): 121 | ^ 122 | SyntaxError: invalid syntax 123 | """ 124 | lines = exc_stdout.splitlines() 125 | assert "SyntaxError" in lines[-1] 126 | assert "^" in lines[-2] 127 | pos = 0 128 | while lines[-2][pos] == " ": 129 | pos += 1 130 | del lines[-2:] 131 | have_foo = False 132 | while lines: 133 | line = lines[-1] 134 | del lines[-1] 135 | if "foo" in line: 136 | have_foo = True 137 | if line.startswith(" " * pos): 138 | continue 139 | assert "line:" in line, "prefix %r, line %r, got:\n%s" % (" " * pos, line, exc_stdout) 140 | break 141 | assert have_foo 142 | assert ".py" in lines[-1] 143 | 144 | 145 | def test_get_source_code_multi_line(): 146 | dummy_fn = "<_test_multi_line_src>" 147 | source_code = "(lambda _x: None)(" 148 | source_code += "__name__,\n" + len(source_code) * " " + "42)\n" 149 | set_linecache(filename=dummy_fn, source=source_code) 150 | 151 | src = get_source_code(filename=dummy_fn, lineno=2) 152 | assert src == source_code 153 | 154 | src = get_source_code(filename=dummy_fn, lineno=1) 155 | assert src == source_code 156 | 157 | 158 | def test_parse_py_statement_prefixed_str(): 159 | # Our parser just ignores the prefix. But that is fine. 160 | code = "b'f(1,'" 161 | statements = list(parse_py_statement(code)) 162 | assert statements == [("str", "f(1,")] 163 | 164 | 165 | def test_exception_chaining(): 166 | if PY2: 167 | return # not supported in Python 2 168 | exc_stdout = _run_code_format_exc( 169 | """ 170 | try: 171 | {}['a'] 172 | except KeyError as exc: 173 | raise ValueError('failed') from exc 174 | """, 175 | ValueError, 176 | ) 177 | assert "The above exception was the direct cause of the following exception" in exc_stdout 178 | assert "KeyError" in exc_stdout 179 | assert "ValueError" in exc_stdout 180 | 181 | 182 | def test_exception_chaining_implicit(): 183 | exc_stdout = _run_code_format_exc( 184 | """ 185 | try: 186 | {}['a'] 187 | except KeyError: 188 | raise ValueError('failed') 189 | """, 190 | ValueError, 191 | ) 192 | if not PY2: # Python 2 does not support this 193 | assert "During handling of the above exception, another exception occurred" in exc_stdout 194 | assert "KeyError" in exc_stdout 195 | assert "ValueError" in exc_stdout 196 | 197 | 198 | def test_pickle_extracted_stack(): 199 | import pickle 200 | import traceback 201 | from better_exchook import _StackSummary_extract 202 | 203 | # traceback.extract_stack(): 204 | # noinspection PyUnresolvedReferences 205 | f = sys._getframe() 206 | stack = _StackSummary_extract(traceback.walk_stack(f)) 207 | assert ( 208 | isinstance(stack, traceback.StackSummary) and len(stack) >= 1 and isinstance(stack[0], traceback.FrameSummary) 209 | ) 210 | assert type(stack[0]) is ExtendedFrameSummary 211 | s = pickle.dumps(stack) 212 | stack2 = pickle.loads(s) 213 | assert ( 214 | isinstance(stack2, traceback.StackSummary) 215 | and len(stack2) == len(stack) 216 | and isinstance(stack2[0], traceback.FrameSummary) 217 | ) 218 | # We ignore the extended frame summary when serializing it. 219 | assert type(stack2[0]) is traceback.FrameSummary 220 | 221 | 222 | def test_extracted_stack_format_len(): 223 | from better_exchook import _StackSummary_extract 224 | import traceback 225 | 226 | # traceback.extract_stack(): 227 | # noinspection PyUnresolvedReferences 228 | f = sys._getframe() 229 | stack = _StackSummary_extract(traceback.walk_stack(f)) 230 | stack_strs = format_tb(stack) 231 | for i, s in enumerate(stack_strs): 232 | print("entry %i:" % i) 233 | print(s, end="") 234 | assert len(stack) == len(stack_strs) >= 1 235 | print("All ok.") 236 | 237 | 238 | def test(): 239 | for k, v in sorted(globals().items()): 240 | if not k.startswith("test_"): 241 | continue 242 | _fold_open(k) 243 | print("running: %s()" % k) 244 | v() 245 | _fold_close() 246 | 247 | print("All ok.") 248 | 249 | 250 | def main(): 251 | """ 252 | Main entry point. Either calls the function, or just calls the demo. 253 | """ 254 | install() 255 | arg_parser = ArgumentParser() 256 | arg_parser.add_argument("command", default=None, help="test, ...", nargs="?") 257 | args = arg_parser.parse_args() 258 | if args.command: 259 | if "test_%s" % args.command in globals(): 260 | func_name = "test_%s" % args.command 261 | elif args.command in globals(): 262 | func_name = args.command 263 | else: 264 | print("Error: Function (test_)%s not found." % args.command) 265 | sys.exit(1) 266 | print("Run %s()." % func_name) 267 | func = globals()[func_name] 268 | func() 269 | sys.exit() 270 | 271 | # Run all tests. 272 | test() 273 | 274 | 275 | if __name__ == "__main__": 276 | main() 277 | --------------------------------------------------------------------------------