diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index ab1871e..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,43 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: build - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install flake8 pytest - python -m pip install numpy - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Install as editable package - run: | - python -m pip install -e . - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/upload_pypi.yml b/.github/workflows/upload_pypi.yml deleted file mode 100644 index 26c464a..0000000 --- a/.github/workflows/upload_pypi.yml +++ /dev/null @@ -1,40 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload Python Package - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - verbose: true - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore deleted file mode 100644 index aa7f212..0000000 --- a/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -__pycache__/ -*.py[cod] -*$py.class - -*.egg-info/ -build/ -dist/ - -.venv*/ -.pytest_cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8a804c0..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -# 0.2.13 - April 14, 2026 - -## Fixed -- Python 3.12+ compatibility: `tb.tb_lineno` and `frame.f_lineno` can return `None` when an instruction has no line mapping (for example at some async suspension points or on synthetic RESUME/CACHE opcodes). `extraction.get_info` now substitutes `frame.f_code.co_firstlineno` in that case instead of crashing with `AssertionError` in `source_inspection.annotate`. - -# 0.2.8 - August 25, 2022 - -## Fixed -Replace deprecated distutils.LooseVersion by packaging.version - -# 0.2.7 - August 12, 2022 - -## Fixed -- Degrade more gracefully in environments where the standard output streams (stdout, stderr) are not available, such as the `pythonw.exe` GUI. Concretely: 1) If stackprinter's `show()` function is called in such an environment and with default arguments, it will now return silently (doing nothing) instead of crashing. 2) the 'Traceprinter' toy now uses the built in print function (so that it doesn't try to access sys.stderr.write on import). - -# 0.2.6 - April 2, 2022 - -## Added -- New kwarg `suppressed_vars` to redact certain variables, e.g. to keep secrets out of error logs - -# 0.2.5 - Oct 31, 2020 - -## Fixed -- Allows passing `(None, None, None)` to `format_exception` -- Fixed a crashing type error that could occur in longer code scopes (e.g. in the repl) - -# 0.2.4 - June 17, 2020 - -## Changed -- Disabled verbose formatting for KeyboardInterrupts by default. Call `format(..., suppressed_exceptions=None`) to enforce verbose printing even on a keyboard interrupt. - -## Added -- New keyword arg `suppressed_exceptions` to disable verbose formatting for certain types of exceptions (generating a standard python-like traceback instead). -- New keyword arg `line_wrap` to adjust or disable the line wrap on variable values. - - -# 0.2.3 - May 29, 2019 - -(beginning of time) diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 487b1f3..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Clemens KorndΓΆrfer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 88d498b..0000000 --- a/README.md +++ /dev/null @@ -1,244 +0,0 @@ - - -[![build](https://github.com/cknd/stackprinter/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/cknd/stackprinter/actions/workflows/build.yml) - -# Better tracebacks - -This is a more helpful version of Python's built-in exception message: It shows more code context and the current values of nearby variables. That answers many of the questions I'd ask an interactive debugger: Where in the code was the crash, what's in the relevant variables, and why was _that_ function called with _those_ arguments. It either prints to the console or gives you a string for logging. - -```bash -pip3 install stackprinter -``` -### Before -``` -Traceback (most recent call last): - File "demo.py", line 12, in - dangerous_function(somelist + anotherlist) - File "demo.py", line 6, in dangerous_function - return sorted(blub, key=lambda xs: sum(xs)) - File "demo.py", line 6, in - return sorted(blub, key=lambda xs: sum(xs)) -TypeError: unsupported operand type(s) for +: 'int' and 'str' -``` - -### After -``` -File demo.py, line 12, in - 9 somelist = [[1,2], [3,4]] - 10 anotherlist = [['5', 6]] - 11 spam = numpy.zeros((3,3)) ---> 12 dangerous_function(somelist + anotherlist) - 13 except: - .................................................. - somelist = [[1, 2, ], [3, 4, ], ] - anotherlist = [['5', 6, ], ] - spam = 3x3 array([[0. 0. 0.] - [0. 0. 0.] - [0. 0. 0.]]) - .................................................. - -File demo.py, line 6, in dangerous_function - 5 def dangerous_function(blub): ---> 6 return sorted(blub, key=lambda xs: sum(xs)) - .................................................. - blub = [[1, 2, ], [3, 4, ], ['5', 6, ], ] - .................................................. - -File demo.py, line 6, in - 3 - 4 - 5 def dangerous_function(blub): ---> 6 return sorted(blub, key=lambda xs: sum(xs)) - 7 - .................................................. - xs = ['5', 6, ] - .................................................. - -TypeError: unsupported operand type(s) for +: 'int' and 'str' -``` -I rarely use this locally instead of a real debugger, but it helps me sleep when my code runs somewhere where the only debug tool is a log file (though it's not a fully-grown [error monitoring system](https://sentry.io/welcome/)). - -By default, it tries to be somewhat polite about screen space, showing only a few source lines & the function header, and only the variables _that appear in those lines_, and using only (?) 500 characters per variable. You can [configure](https://github.com/cknd/stackprinter/blob/master/stackprinter/__init__.py#L28-L149) exactly how verbose things should be. - -It outputs plain text normally, which is good for log files. There's also a color mode for some reason 🌈, with a few different color schemes for light and dark backgrounds. (The colors [track different variables](https://medium.com/@brianwill/making-semantic-highlighting-useful-9aeac92411df) instead of the language syntax.) - - - -# Usage - -## Option 1: Set and forget -To replace the default python crash printout, call `set_excepthook()` somewhere once. Afterwards, any uncaught exception will be printed with an extended traceback (to stderr, by default). You could also [make this permanent for your python installation](#making-it-stick). - -```python -import stackprinter -stackprinter.set_excepthook(style='darkbg2') # for jupyter notebooks try style='lightbg' -``` -## Option 2: Call it selectively during exception handling -For more control, call [`show()`](https://github.com/cknd/stackprinter/blob/master/stackprinter/__init__.py#L166-L183) or [`format()`](https://github.com/cknd/stackprinter/blob/master/stackprinter/__init__.py#L28-L149) inside an `except` block. `show()` prints to stderr, `format()` returns a string, for custom logging. - -```python -try: - something() -except: - # print the current exception to stderr: - stackprinter.show() - - # ...or instead, get a string for logging: - logger.error(stackprinter.format()) -``` -Or pass specific exceptions explicitly: -```python -try: - something() -except RuntimeError as exc: - tb = stackprinter.format(exc) - logger.error('The front fell off.\n' + tb) -``` - -## Config options - -`format()`, `show()` and `set_excepthook()` accept a common set of keyword args. They allow you to tweak the formatting, hide certain variables by name, skip variables in calls within certain files, and some other stuff. - -```python -try: - something() -except RuntimeError as exc: - stackprinter.show(exc, suppressed_vars=[r".*secret.*"], - suppressed_paths=[r"lib/python.*/site-packages/boringstuff"], - truncate_vals=9001, - style="darkbg2") -``` -For all the options [see the docstring of `format()`](https://github.com/cknd/stackprinter/blob/master/stackprinter/__init__.py#L28-L149). - -## Option 3: Integrate with the standard `logging` module - -With a bit of extra plumbing you can log errors like this via the normal `logging` methods, without having to import `stackprinter` at the site of the logging call. So you can continue to write nice and simple error handlers like so... - -```python -logger = logging.getLogger() -try: - nothing = {} - dangerous_function(nothing.get("something")) -except: - logger.exception('My hovercraft is full of eels.') -``` -...but still get annotated tracebacks in the resulting log. -``` -2022-04-02 16:16:40,905 ERROR: My hovercraft is full of eels. - ┆ File "demo_logging.py", line 56, in - ┆ 54 try: - ┆ 55 nothing = {} - ┆ --> 56 dangerous_function(nothing.get("something")) - ┆ 57 except: - ┆ .................................................. - ┆ nothing = {} - ┆ .................................................. - ┆ - ┆ File "demo_logging.py", line 52, in dangerous_function - ┆ 51 def dangerous_function(something): - ┆ --> 52 return something + 1 - ┆ .................................................. - ┆ something = None - ┆ .................................................. - ┆ - ┆ TypeError: unsupported operand type(s) for +: 'NoneType' and 'int' -``` - -You can get this by adding a [custom formatter](https://docs.python.org/3/howto/logging-cookbook.html#customized-exception-formatting) to the logger before using it: - -```python -import logging -import stackprinter - -class VerboseExceptionFormatter(logging.Formatter): - def formatException(self, exc_info): - msg = stackprinter.format(exc_info) - lines = msg.split('\n') - lines_indented = [" ┆ " + line + "\n" for line in lines] - msg_indented = "".join(lines_indented) - return msg_indented - -def configure_logger(logger_name=None): - fmt = '%(asctime)s %(levelname)s: %(message)s' - formatter = VerboseExceptionFormatter(fmt) - - handler = logging.StreamHandler() - handler.setFormatter(formatter) - - logger = logging.getLogger(logger_name) - logger.addHandler(handler) - -configure_logger("some_logger") -``` -See [demo_logging.py](https://github.com/cknd/stackprinter/blob/master/demo_logging.py) for a runnable example. - -## Printing the current call stack -To see your own thread's current call stack, call `show` or `format` anywhere outside of exception handling. - -```python -stackprinter.show() # or format() -``` - -## Printing the stack of another thread -To inspect the call stack of any other running thread: - -```python -thread = threading.Thread(target=something) -thread.start() -# (...) -stackprinter.show(thread) # or format(thread) -``` - -## Making it stick - -To permanently replace the crash message for your python installation, you *could* put a file `sitecustomize.py` into the `site-packages` directory under one of the paths revealed by `python -c "import site; print(site.PREFIXES)"`, with contents like this: - -```python - # in e.g. some_virtualenv/lib/python3.x/site-packages/sitecustomize.py: - import stackprinter - stackprinter.set_excepthook(style='darkbg2') -``` - -That would give you colorful tracebacks automatically every time, even in the REPL. - -(You could do a similar thing for IPython, [but they have their own method](https://ipython.readthedocs.io/en/stable/interactive/tutorial.html?highlight=startup#configuration), where the file goes into `~/.ipython/profile_default/startup` instead, and also I don't want to talk about what this module does to set an excepthook under IPython.) - -# Docs - -For now, the documentation consists only of some fairly detailed docstrings, [e.g. those of `format()`](https://github.com/cknd/stackprinter/blob/master/stackprinter/__init__.py#L28-L149) - -# Caveats - -This displays variable values as they are _at the time of formatting_. In -multi-threaded programs, variables can change while we're busy walking -the stack & printing them. So, if nothing seems to make sense, consider that -your exception and the traceback messages are from slightly different times. -Sadly, there is no responsible way to freeze all other threads as soon -as we want to inspect some thread's call stack (...or is there?) - -# How it works - -Basically, this is a frame formatter. For each [frame on the call stack](https://en.wikipedia.org/wiki/Call_stack), it grabs the source code to find out which source lines reference which variables. Then it displays code and variables in the neighbourhood of the last executed line. - -Since this already requires a map of where each variable occurs in the code, it was difficult not to also implement the whole semantic highlighting color thing seen in the screenshots. The colors are ANSI escape codes now, but it should be fairly straightforwardβ„’ to render the underlying data without any 1980ies terminal technology. Say, a foldable and clickable HTML page with downloadable pickled variables. For now you'll have to pipe the ANSI strings through [ansi2html](https://github.com/ralphbean/ansi2html/) or something. - -The format and everything is inspired by the excellent [`ultratb`](https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.ultratb.html) in IPython. One day I'd like to contribute the whole "find out which variables in `locals` and `globals` are nearby in the source and print only those" machine over there, after trimming its complexity a bit. - -## Tracing a piece of code - -More for curiosity than anything else, you can watch a piece of code execute step-by-step, printing a trace of all calls & returns 'live' as they are happening. Slows everything down though, of course. -```python -with stackprinter.TracePrinter(style='darkbg2'): - dosomething() -``` - -or -```python -tp = stackprinter.TracePrinter(style='darkbg2') -tp.enable() -dosomething() -# (...) +1 million lines -tp.disable() -``` - - diff --git a/darkbg.png b/darkbg.png deleted file mode 100644 index f8503ca..0000000 Binary files a/darkbg.png and /dev/null differ diff --git a/demo.py b/demo.py deleted file mode 100644 index b64f8d0..0000000 --- a/demo.py +++ /dev/null @@ -1,16 +0,0 @@ -import numpy -import stackprinter - - -def dangerous_function(blub): - return sorted(blub, key=lambda xs: sum(xs)) - -try: - somelist = [[1,2], [3,4]] - anotherlist = [['5', 6]] - spam = numpy.zeros((3,3)) - supersecret = "you haven't seen this" - dangerous_function(somelist + anotherlist) -except: - stackprinter.show(style='plaintext', source_lines=4, suppressed_vars=[r".*secret.*"]) - diff --git a/demo_chained_exceptions.py b/demo_chained_exceptions.py deleted file mode 100644 index 133d3ca..0000000 --- a/demo_chained_exceptions.py +++ /dev/null @@ -1,18 +0,0 @@ -import stackprinter -stackprinter.set_excepthook() - - -def bomb(msg): - try: - raise Exception(msg) - except Exception as e: - raise Exception(msg+'_b') from e - -try: - bomb('1') -except Exception as e: - try: - raise Exception('2') from e - except: - bomb('3') - diff --git a/demo_complex.py b/demo_complex.py deleted file mode 100644 index 2d1d191..0000000 --- a/demo_complex.py +++ /dev/null @@ -1,13 +0,0 @@ -if __name__ == '__main__': - import stackprinter - from tests.source import Hovercraft - - try: - Hovercraft().eels() - except: - # raise - stackprinter.show(style='darkbg2', - line_wrap=40, - reverse=False, - suppressed_paths=[r"lib/python.*/site-packages/numpy"], - suppressed_vars=[r".*secret.*"]) diff --git a/demo_exceptiongroups.py b/demo_exceptiongroups.py deleted file mode 100644 index ff633f5..0000000 --- a/demo_exceptiongroups.py +++ /dev/null @@ -1,16 +0,0 @@ -# flake8: noqa - -# Exception groups (new in py 3.11) aren't supported so far, -# but at least we fall back on the default message. - -import stackprinter -stackprinter.set_excepthook() - - -def raise_group(): - group = ExceptionGroup('A group!', - [Exception("Something!"), Exception("Something else!")]) - raise group - - -raise_group() diff --git a/demo_keyboard_interrupt.py b/demo_keyboard_interrupt.py deleted file mode 100644 index 68c49ce..0000000 --- a/demo_keyboard_interrupt.py +++ /dev/null @@ -1,19 +0,0 @@ -import time -import stackprinter - - -def blocking_function(x): - print('ctrl-C me') - time.sleep(5) - -try: - some_value = 'spam' - blocking_function(some_value) -except: - # Default: Only print summary info - stackprinter.show() - # Override: - # stackprinter.show(suppressed_exceptions=None) - # stackprinter.show(suppressed_exceptions=[]) - - diff --git a/demo_logging.py b/demo_logging.py deleted file mode 100644 index ef60396..0000000 --- a/demo_logging.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Custom exception formatter example -based on https://docs.python.org/3/howto/logging-cookbook.html#customized-exception-formatting - -The goal is to add detailed traces to standard `logging` calls, e.g. - - try: - something() - except: - logger.exception('The front fell off.') - -""" - -import logging - - -# =================== Setup ======================= # -import stackprinter - -class VerboseExceptionFormatter(logging.Formatter): - def formatException(self, exc_info): - msg = stackprinter.format(exc_info) - lines = msg.split('\n') - lines_indented = [" ┆ " + line + "\n" for line in lines] - msg_indented = "".join(lines_indented) - return msg_indented - -def configure_logger(logger_name=None): - fmt = '%(asctime)s %(levelname)s: %(message)s' - formatter = VerboseExceptionFormatter(fmt) - - - handler = logging.StreamHandler() - handler.setFormatter(formatter) - - # # Add more, like - # handler = logging.FileHandler('log.txt') - # ... - - logger = logging.getLogger(logger_name) - logger.addHandler(handler) - - -configure_logger("some_logger") - - -# =================== Use ======================= # -print("\n\n") -logger = logging.getLogger("some_logger") - -def dangerous_function(something): - return something + 1 - -try: - nothing = {} - dangerous_function(nothing.get("something")) -except: - logger.exception('My hovercraft is full of eels.') - # Or equivalently: - # logger.error('My hovercraft is full of eels.', exc_info=True) - - diff --git a/demo_logging_hack.py b/demo_logging_hack.py deleted file mode 100644 index bc5a756..0000000 --- a/demo_logging_hack.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import stackprinter - -def patch_logging(**kwargs): - """ - Replace `formatException` on every log handler / formatter we can find - - **kwargs are those of stackprinter.format - - """ - # this is based on https://github.com/Qix-/better-exceptions/blob/master/better_exceptions/log.py - - def format_exc(exc_info): - msg = stackprinter.format(exc_info, **kwargs) - msg_indented = ' ' + '\n '.join(msg.split('\n')).strip() - return msg_indented - - if hasattr(logging, '_defaultFormatter'): - logging._defaultFormatter.formatException = format_exc - - handlers = [handler_ref() for handler_ref in logging._handlerList] - - is_patchable = lambda handler: handler.formatter is not None - - patchable_handlers = filter(is_patchable, handlers) - - for hd in patchable_handlers: - hd.formatter.formatException = format_exc - -# option A: use the root loger: -logger = logging.getLogger() - -# # option B: use a custom one: -# logging.basicConfig() -# logger = logging.getLogger('some logger') - -patch_logging(style='darkbg') - -#### test: - -def dangerous_function(blub): - return sorted(blub, key=lambda xs: sum(xs)) - -try: - somelist = [[1,2], [3,4]] - anotherlist = [['5', 6]] - dangerous_function(somelist + anotherlist) -except: - logger.exception('the front fell off.') - - diff --git a/demo_pprint.py b/demo_pprint.py deleted file mode 100644 index 23dfe67..0000000 --- a/demo_pprint.py +++ /dev/null @@ -1,35 +0,0 @@ -if __name__ == '__main__': - import time - import sys - import numpy as np - from stackprinter import format - - - def bump(): - print('creating crazy data structures.') - boing = {k:np.ones((100,100)) for k in range(int(2e4))} - print('.') - somelist = [1,2,3,boing, boing, 4,5,6,7,8,9] * int(1e5) - print('done.') - - somedict = {'various': 'excellent', - 123: 'things', - 'in': np.ones((42,23)), - 'a list': somelist} - sometuple = (1,2, somedict, np.ones((32,64)), boing) - somedict['recursion'] = somedict - raise Exception('hello') - - - try: - bump() - except: - stuff = sys.exc_info() - scopidoped = 'gotcha' - tic = time.perf_counter() - - msg = format(stuff, style='color', show_vals='all', reverse=False, truncate_vals=250, suppressed_paths=["site-packages"]) - - took = time.perf_counter() - tic - print(msg) - print('took %.1fms' % (took * 1000)) diff --git a/demo_thread_stack.py b/demo_thread_stack.py deleted file mode 100644 index 6a2196b..0000000 --- a/demo_thread_stack.py +++ /dev/null @@ -1,35 +0,0 @@ -import sys -import time -from threading import Thread -from stackprinter import format - -def forever(): - x = 0 - while True: - x += 1 - y = x % 2 - if y == 0: - assert y == 0 - dosomething(x) - else: - assert y != 0 - dosomethingelse(x) - - -def dosomething(x): - time.sleep(1./x) - - -def dosomethingelse(x): - time.sleep(1./x) - - -thr = Thread(name='boing', target=forever, daemon=True) - -thr.start() - - -while True: - print(chr(27) + "[2J") # clear screen - print(format(thr, style='color', source_lines='all', reverse=True, add_summary=True)) - time.sleep(0.1) \ No newline at end of file diff --git a/demo_traceprinter.py b/demo_traceprinter.py deleted file mode 100644 index 9bce93f..0000000 --- a/demo_traceprinter.py +++ /dev/null @@ -1,25 +0,0 @@ -from stackprinter import trace, TracePrinter -import numpy as np - -def dosomething(x): - y = dosomethingelse(x) - return y - -def dosomethingelse(y): - a = 2*y - b = doYetAnotherThing(y) - # raise Exception('ahoi') - return b - -def doYetAnotherThing(z): - a = z - b = {'a': np.ones(1)} - zup = np.ones(0) - return zup - - - -# with TracePrinter(style='color', suppressed_paths=[r"lib/python.*/site-packages/numpy"]): -with TracePrinter(style='plaintext'): - a = np.ones(123) - dosomething(a) diff --git a/notebook.png b/notebook.png deleted file mode 100644 index 1569195..0000000 Binary files a/notebook.png and /dev/null differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0589ede --- /dev/null +++ b/readme.md @@ -0,0 +1,51 @@ +when your only debugger is a log file, better add some extra-verbose traceback formatting (a bit of source context & the current local variables) + + +### Before: + +``` +Traceback (most recent call last): + File "tracebacks.py", line 130, in some_function + a_broken_function(thing) + File "tracebacks.py", line 126, in a_broken_function + raise Exception('something happened') +Exception: something happened + +``` +### After: + +``` +File tracebacks.py, line 129 in some_function + 127 def some_function(boing, zap='!'): + 128 thing = boing + zap +--> 129 a_broken_function(thing) + .................................................. + boing = 'hello' + thing = 'hello!' + zap = '!' + .................................................. + + +File tracebacks.py, line 125 in a_broken_function + 117 def a_broken_function(thing, otherthing=1234): + (...) + 120 # and various weird variables + 121 X = np.zeros((5, 5)) + 122 X[0] = len(thing) + 123 for k in X: + 124 if np.sum(k) != 0: +--> 125 raise Exception('something happened') + .................................................. + X = array([[6., 6., 6., 6., 6.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + k = array([6., 6., 6., 6., 6.]) + otherthing = 1234 + thing = 'hello!' + .................................................. +Exception: something happened + + +``` diff --git a/setup.py b/setup.py deleted file mode 100644 index f902320..0000000 --- a/setup.py +++ /dev/null @@ -1,22 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - python_requires=">=3.4", - name="stackprinter", - version="0.2.13", - author="cknd", - author_email="ck-github@mailbox.org", - description="Debug-friendly stack traces, with variable values and semantic highlighting", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/cknd/stackprinter", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], -) diff --git a/stackprinter/__init__.py b/stackprinter/__init__.py deleted file mode 100644 index 94ae70c..0000000 --- a/stackprinter/__init__.py +++ /dev/null @@ -1,356 +0,0 @@ -import sys -import types -import warnings -from threading import Thread -from functools import wraps - -import stackprinter.formatting as fmt -from stackprinter.tracing import TracePrinter, trace - - -def _guess_thing(f): - """ default to the current exception or current stack frame""" - - # the only reason this happens up here is to keep sys._getframe at the same - # call depth relative to an invocation of `show` or `format`, even when - # `format` is called _by_ `show`. - @wraps(f) - def show_or_format(thing=None, *args, **kwargs): - if thing is None: - thing = sys.exc_info() - if thing == (None, None, None): - thing = sys._getframe(1) - return f(thing, *args, **kwargs) - return show_or_format - - -@_guess_thing -def format(thing=None, **kwargs): - r""" - Render the traceback of an exception or a frame's call stack - - - Call this without arguments inside an `except` block to get a traceback for - the currently handled exception: - ``` - try: - something() - except: - logger.err(stackprinter.format(**kwargs)) - ``` - - Explicitly pass an exception (or a triple as returned by `sys.exc_info()`) - to handle that particular exception anywhere, also outside an except block. - ``` - try: - something() - except Exception as e: - last_exc = e - - if last_exc: - logger.err(stackprinter.format(last_exc, **kwargs)) - ``` - - Pass a frame object to see the call stack leading up to that frame: - ``` - stack = stackprinter.format(sys._getframe(2), **kwargs)) - ``` - - Pass a thread object to see its current call stack: - ``` - thread = threading.Thread(target=something) - thread.start() - # (...) - stack = stackprinter.format(thread, **kwargs)) - ``` - - Note: - This displays variable values as they are _at the time of formatting_. In - multi-threaded programs, variables can change while we're busy walking - the stack & printing them. So, if nothing seems to make sense, consider that - your exception and the traceback messages are from slightly different times. - Sadly, there is no responsible way to freeze all other threads as soon - as we want to inspect some thread's call stack (...or is there?) - - - Params - --- - thing: (optional) exception, sys.exc_info() tuple, frame or thread - What to format. Defaults to the currently handled exception or current - stack frame. - - style: string - 'plaintext' (default): Output just text - - 'darkbg', 'darkbg2', 'darkbg3', 'lightbg', 'lightbg2', 'lightbg3': - Enable colors, for use in terminals that support 256 ansi - colors or in jupyter notebooks (or even with `ansi2html`) - - source_lines: int or 'all' - Select how much source code context will be shown. - int 0: Don't include a source listing. - int n > 0: Show n lines of code. (default: 5) - string 'all': Show the whole scope of the frame. - - show_signature: bool (default True) - Always include the function header in the source code listing. - - show_vals: str or None - Select which variable values will be shown. - 'line': Show only the variables on the highlighted line. - 'like_source' (default): Show only those visible in the source listing - 'all': Show every variable in the scope of the frame. - None: Don't show any variable values. - - truncate_vals: int - Maximum number of characters to be used for each variable value. - Default: 500 - - line_wrap: int (default 60) - Limit how many columns are available to print each variable - (excluding its name). Set to 0 or False to disable wrapping. - - suppressed_paths: list of regex patterns - Set less verbose formatting for frames whose code lives in certain paths - (e.g. library code). Files whose path matches any of the given regex - patterns will be considered boring. The first call to boring code is - rendered with fewer code lines (but with argument values still visible), - while deeper calls within boring code get a single line and no variable - values. - - Example: To hide numpy internals from the traceback, set - `suppressed_paths=[r"lib/python.*/site-packages/numpy"]` - or - `suppressed_paths=[re.compile(r"lib/python.*/site-packages/numpy")]` - - suppressed_exceptions: list of exception classes - Show less verbose formatting for exceptions in this list. - By default, this list is `[KeyboardInterrupt]`. Set to `[]` - to force verbose formatting even on a keyboard interrupt. - - suppressed_vars: list of regex patterns - Don't show the content of variables whose name matches any of the given - patterns. - Internally, this doesn't just filter the output, but stackprinter won't - even try to access these values at all. So this can also be used as a - workaround for rare issues around dynamic attribute lookups. - - Example: - `suppressed_vars=[r".*password.*", r"certainobject\.certainproperty"]` - - reverse: bool - List the innermost frame first. - - add_summary: True, False, 'auto' - Append a compact list of involved files and source lines, similar - to the built-in traceback message. - 'auto' (default): do that if the main traceback is longer than 50 lines. - - """ - if isinstance(thing, types.FrameType): - return fmt.format_stack_from_frame(thing, **kwargs) - elif isinstance(thing, Thread): - return format_thread(thing, **kwargs) - elif isinstance(thing, Exception): - exc_info = (thing.__class__, thing, thing.__traceback__) - return format(exc_info, **kwargs) - elif _is_exc_info(thing): - return fmt.format_exc_info(*thing, **kwargs) - else: - raise ValueError("Can't format %s. "\ - "Expected an exception instance, sys.exc_info() tuple,"\ - "a frame or a thread object." % repr(thing)) - - -@_guess_thing -def show(thing=None, file='stderr', **kwargs): - """ - Print the traceback of an exception or a frame's call stack - - Params - --- - file: 'stderr', 'stdout' or file-like object - defaults to stderr - - **kwargs: - See `format` - """ - - # First, to handle a very rare edge case: - # Apparently there are environments where sys.stdout and stderr - # are None (like the pythonw.exe GUI https://stackoverflow.com/a/30313091). - # In those cases, it's not clear where our output should go unless the user - # specifies their own file for output. So I'll make a pragmatic assumption: - # If `show` is called with the default 'stderr' argument but we are in an - # environment where that stream doesn't exist, we're most likely running as - # part of a library that's imported in someone's GUI project and there just - # isn't any error logging (if there was, the user would've given us a file). - # So the least annoying behavior for us is to return silently, not crashing. - if file == 'stderr' and sys.stderr is None: - return - - if file == 'stderr': - file = sys.stderr - elif file == 'stdout': - file = sys.stdout - - print(format(thing, **kwargs), file=file) - - - -def format_current_stack(**kwargs): - """ Render the current thread's call stack. - - Params - -- - **kwargs: - See `format` - """ - return format(sys._getframe(1), **kwargs) - -def show_current_stack(**kwargs): - """ Print the current thread's call stack. - - Params - -- - **kwargs: - See `show` - """ - show(sys._getframe(1), **kwargs) - - - - -def format_current_exception(**kwargs): - """ - Render a traceback for the currently handled exception. - - Params - -- - **kwargs: - See `format` - """ - return format(sys.exc_info(), **kwargs) - -def show_current_exception(file=sys.stderr, **kwargs): - """ - Print a traceback for the currently handled exception. - - Params - -- - **kwargs: - See `show` - """ - if file is None: - return # see explanation in `show()` - print(format_current_exception(**kwargs), file=file) - - - -def set_excepthook(**kwargs): - """ - Set sys.excepthook to print a detailed traceback for any uncaught exception. - - See `format()` for available kwargs. - - Examples: - ---- - - Print to stdout instead of stderr: - ``` - set_excepthook(file='stdout') - ``` - - Enable color output: - ``` - set_excepthook(style='darkbg') # or e.g. 'lightbg' (for more options see `format`) - ``` - - If running under Ipython, this will, with a heavy heart, attempt to monkey - patch Ipython's traceback printer (which handles all exceptions internally, - thus bypassing the system excepthook). You can decide whether this sounds - like a sane idea. - - To undo, call `remove_excepthook`. - - Params - -- - **kwargs: - See `show` and `format` - """ - if _is_running_in_ipython(): - _patch_ipython_excepthook(**kwargs) - else: - def hook(*args): - show(args, **kwargs) - - sys.excepthook = hook - - -def remove_excepthook(): - """ Reinstate the default excepthook """ - if _is_running_in_ipython(): - _unpatch_ipython_excepthook() - sys.excepthook = sys.__excepthook__ - - -def _is_running_in_ipython(): - try: - return __IPYTHON__ - except NameError: - return False - - -ipy_tb = None - -def _patch_ipython_excepthook(**kwargs): - """ Replace ipython's built-in traceback printer, excellent though it is""" - global ipy_tb - - blacklist = kwargs.get('suppressed_paths', []) - blacklist.append('site-packages/IPython/') - kwargs['suppressed_paths'] = blacklist - - if 'file' in kwargs: - del kwargs['file'] - - def format_tb(*exc_tuple, **__): - unstructured_tb = format(exc_tuple, **kwargs) - structured_tb = [unstructured_tb] # \*coughs* - return structured_tb - - import IPython - shell = IPython.get_ipython() - if ipy_tb is None: - ipy_tb = shell.InteractiveTB.structured_traceback - shell.InteractiveTB.structured_traceback = format_tb - - -def _unpatch_ipython_excepthook(): - """ restore proper order in Ipython """ - import IPython - shell = IPython.get_ipython() - if ipy_tb is not None: - shell.InteractiveTB.structured_traceback = ipy_tb - - -def _is_exc_info(thing): - if not isinstance(thing, tuple) or len(thing) != 3: - return False - a, b, c = thing - return ((a is None or (isinstance(a, type) and BaseException in a.mro())) and - (b is None or (isinstance(b, BaseException)))) - -def format_thread(thread, add_summary=False, **kwargs): - try: - fr = sys._current_frames()[thread.ident] - except KeyError: - return "%r: no frames found" % thread - else: - if 'suppressed_paths' not in kwargs: - kwargs['suppressed_paths'] = [] - kwargs['suppressed_paths'] += [r"lib/python.*/threading\.py"] - - msg = fmt.format_stack_from_frame(fr, **kwargs) - msg_indented = ' ' + '\n '.join(msg.split('\n')).strip() - return "%r\n\n%s" % (thread, msg_indented) diff --git a/stackprinter/colorschemes.py b/stackprinter/colorschemes.py deleted file mode 100644 index 4001ae1..0000000 --- a/stackprinter/colorschemes.py +++ /dev/null @@ -1,251 +0,0 @@ -import random - - -__all__ = ['color', 'darkbg', 'darkbg2', 'darkbg3', - 'lightbg', 'lightbg2', 'lightbg3'] - -class ColorScheme(): - - def __getitem__(self, name): - raise NotImplemented - - def get_random(self): - raise NotImplemented - - -class darkbg(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0.0, 0.9, 0.6, False), - 'exception_msg': (0.0, 0.9, 0.6, True), - - 'highlight': (0.0, 0., 0.8, True), - 'header': (0., 0., 0.3, False), - - 'lineno': (0., 0.0, 0.1, False), - 'arrow_lineno': (0., 0.0, 0.2, True), - 'dots': (0., 0.0, 0.6, False), - - 'source_bold': (0.,0., 0.6, True), - 'source_default': (0.,0., 0.7, False), - 'source_comment': (0.,0.,0.2, False), - 'var_invisible': (0.6, 0.4, 0.4, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05,0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. #1. if highlight else 0.5 - val = 0.5 #1. if highlight else 0.3 - bold = highlight - - return hue, sat, val, bold - - - -class darkbg2(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0., 1., 0.8, True), - 'exception_msg': (0., 1., 0.8, True), - - 'highlight': (0., 0., 1., True), - 'header': (0, 0, 0.6, False), - - 'lineno': (0, 0, 0.2, True), - 'arrow_lineno': (0, 0, 0.8, True), - 'dots': (0, 0, 0.4, False), - - 'source_bold': (0.,0.,0.8, True), - 'source_default': (0.,0.,0.8, False), - 'source_comment': (0.,0.,0.2, False), - 'var_invisible': (0.6, 0.4, 0.4, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05,0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. if highlight else 1. - val = 0.8 #if highlight else 0.5 - bold = highlight - - return hue, sat, val, bold - - -class darkbg3(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0., 1., 0.8, True), - 'exception_msg': (0., 1., 0.8, True), - 'highlight': (0., 1., 0.8, True), - 'header': (0, 0, 0.8, True), - 'lineno': (0, 0, 0.2, True), - 'arrow_lineno': (0, 0, 0.8, True), - 'dots': (0, 0, 0.4, False), - 'source_bold': (0.,0.,0.8, True), - 'source_default': (0.,0.,0.8, False), - 'source_comment': (0.,0.,0.2, False), - 'var_invisible': (0.6, 0.4, 0.4, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05,0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. if highlight else 1. - val = 0.8 if highlight else 0.5 - bold = highlight - - return hue, sat, val, bold - - -class lightbg(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0.0, 1., 0.6, False), - 'exception_msg': (0.0, 1., 0.6, True), - - 'highlight': (0.0, 0, 0., True), - 'header': (0, 0, 0.2, False), - - 'lineno': (0, 0, 0.8, True), - 'arrow_lineno': (0, 0, 0.3, True), - 'dots': (0, 0, 0.4, False), - 'source_bold': (0.,0.,0.2, True), - 'source_default': (0.,0.,0.1, False), - 'source_comment': (0.,0.,0.6, False), - 'var_invisible': (0.6, 0.4, 0.2, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05, 0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. - val = 0.5 #0.5 #0.6 if highlight else 0.2 - bold = highlight - - return hue, sat, val, bold - - -class lightbg2(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0.0, 1., 0.6, False), - 'exception_msg': (0.0, 1., 0.6, True), - - 'highlight': (0.0, 0, 0., True), - 'header': (0, 0, 0.1, False), - - 'lineno': (0, 0, 0.5, True), - 'arrow_lineno': (0, 0, 0.1, True), - 'dots': (0, 0, 0.4, False), - - 'source_bold': (0.,0.,0.1, True), - 'source_default': (0.,0.,0., False), - 'source_comment': (0.,0.,0.6, False), - 'var_invisible': (0.6, 0.4, 0.2, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05, 0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. - val = 0.5 - bold = True - - return hue, sat, val, bold - -class lightbg3(ColorScheme): - # Hue, Sat, Val, Bold - colors = {'exception_type': (0.0, 1., 0.7, False), - 'exception_msg': (0.0, 1., 0.7, True), - - 'highlight': (0.0, 1., 0.6, True), - 'header': (0, 0, 0.1, True), - - 'lineno': (0, 0, 0.5, True), - 'arrow_lineno': (0, 0, 0.1, True), - 'dots': (0, 0, 0.4, False), - - 'source_bold': (0.,0.,0., True), - 'source_default': (0.,0.,0., False), - 'source_comment': (0.,0.,0.6, False), - 'var_invisible': (0.6, 0.4, 0.2, False) - } - - def __init__(self): - self.rng = random.Random() - - def __getitem__(self, name): - return self.colors[name] - - def get_random(self, seed, highlight): - self.rng.seed(seed) - - hue = self.rng.uniform(0.05, 0.7) - # if hue < 0: - # hue = hue + 1 - sat = 1. - val = 0.5 - bold = True - - return hue, sat, val, bold - - - -color = darkbg2 - - -if __name__ == '__main__': - import numpy as np - from utils import get_ansi_tpl - - for hue in np.arange(0,1.05,0.05): - print('\n\nhue %.2f\nsat' % hue) - for sat in np.arange(0,1.05,0.05): - print('%.2f ' % sat, end='') - for val in np.arange(0,1.05,0.05): - tpl = get_ansi_tpl(hue, sat, val) - # number = " (%.1f %.1f %.1f)" % (hue, sat, val) - number = ' %.2f' % val - print(tpl % number, end='') - print(' %.2f' % sat) diff --git a/stackprinter/extraction.py b/stackprinter/extraction.py deleted file mode 100644 index aef1317..0000000 --- a/stackprinter/extraction.py +++ /dev/null @@ -1,209 +0,0 @@ -import types -import inspect -from collections import OrderedDict, namedtuple -from stackprinter.source_inspection import annotate -from stackprinter.utils import match - -NON_FUNCTION_SCOPES = ['', '', ''] - -_FrameInfo = namedtuple('_FrameInfo', - ['filename', 'function', 'lineno', 'source_map', - 'head_lns', 'line2names', 'name2lines', 'assignments']) - -class FrameInfo(_FrameInfo): - # give this namedtuple type a friendlier string representation - def __str__(self): - return ("" % - (self.filename, self.lineno, self.function)) - - -def get_info(tb_or_frame, lineno=None, suppressed_vars=[]): - """ - Get a frame representation that's easy to format - - - Params - --- - tb: Traceback object or Frame object - - lineno: int (optional) - Override which source line is treated as the important one. For trace- - back objects this defaults to the last executed line (tb.tb_lineno). - For frame objects, it defaults the currently executed one (fr.f_lineno). - - - Returns - --- - FrameInfo, a named tuple with the following fields: - - filename: Path of the executed source file - - function: Name of the scope - - lineno: Highlighted line (last executed line) - - source_map: OrderedDict - Maps line numbers to a list of tokens. Each token is a (string, type) - tuple. Concatenating the first elements of all tokens of all lines - restores the original source, weird whitespaces/indentations and all - (in contrast to python's built-in `tokenize`). However, multiline - statements (those with a trailing backslash) are secretly collapsed - into their first line. - - head_lns: (int, int) or (None, None) - Line numbers of the beginning and end of the function header - - line2names: dict - Maps each line number to a list of variables names that occur there - - name2lines: dict - Maps each variable name to a list of line numbers where it occurs - - assignments: OrderedDict - Holds current values of all variables that occur in the source and - are found in the given frame's locals or globals. Attribute lookups - with dot notation are treated as one variable, so if `self.foo.zup` - occurs in the source, this dict will get a key 'self.foo.zup' that - holds the fully resolved value. - (TODO: it would be easy to return the whole attribute lookup chain, - so maybe just do that & let formatting decide which parts to show?) - (TODO: Support []-lookups just like . lookups) - """ - - if isinstance(tb_or_frame, FrameInfo): - return tb_or_frame - - if isinstance(tb_or_frame, types.TracebackType): - tb = tb_or_frame - lineno = tb.tb_lineno if lineno is None else lineno - frame = tb.tb_frame - elif isinstance(tb_or_frame, types.FrameType): - frame = tb_or_frame - lineno = frame.f_lineno if lineno is None else lineno - else: - raise ValueError('Cant inspect this: ' + repr(tb_or_frame)) - - # Since CPython 3.12, both `tb.tb_lineno` and `frame.f_lineno` can return - # None when the current instruction has no line mapping (for example at - # certain async suspension points, or on synthetic RESUME/CACHE opcodes). - # Fall back to the function's first line so we can still render a frame - # rather than crashing downstream formatters with a None lineno. - if lineno is None: - lineno = frame.f_code.co_firstlineno - - filename = inspect.getsourcefile(frame) or inspect.getfile(frame) - function = frame.f_code.co_name - - try: - source, startline = get_source(frame) - # this can be slow (tens of ms) the first time it is called, since - # inspect.get_source internally calls inspect.getmodule, for no - # other purpose than updating the linecache. seems like a bad tradeoff - # for our case, but this is not the time & place to fork `inspect`. - except: - source = [] - startline = lineno - - source_map, line2names, name2lines, head_lns, lineno = annotate(source, startline, lineno) - - if function in NON_FUNCTION_SCOPES: - head_lns = [] - - names = name2lines.keys() - assignments = get_vars(names, frame.f_locals, frame.f_globals, suppressed_vars) - - finfo = FrameInfo(filename, function, lineno, source_map, head_lns, - line2names, name2lines, assignments) - return finfo - - -def get_source(frame): - """ - get source lines for this frame - - Params - --- - frame : frame object - - Returns - --- - lines : list of str - - startline : int - location of lines[0] in the original source file - """ - - # TODO find out what's faster: Allowing inspect's getsourcelines - # to tokenize the whole file to find the surrounding code block, - # or getting the whole file quickly via linecache & feeding all - # of it to our own instance of tokenize, then clipping to - # desired context afterwards. - - if frame.f_code.co_name in NON_FUNCTION_SCOPES: - lines, _ = inspect.findsource(frame) - startline = 1 - else: - lines, startline = inspect.getsourcelines(frame) - - return lines, startline - - -def get_vars(names, loc, glob, suppressed_vars): - assignments = [] - for name in names: - if match(name, suppressed_vars): - assignments.append((name, CensoredVariable())) - else: - try: - val = lookup(name, loc, glob) - except LookupError: - pass - else: - assignments.append((name, val)) - return OrderedDict(assignments) - - -def lookup(name, scopeA, scopeB): - basename, *attr_path = name.split('.') - if basename in scopeA: - val = scopeA[basename] - elif basename in scopeB: - val = scopeB[basename] - else: - # not all names in the source file will be - # defined (yet) when we get to see the frame - raise LookupError(basename) - - for k, attr in enumerate(attr_path): - try: - val = getattr(val, attr) - except Exception as e: - # return a special value in case of lookup errors - # (note: getattr can raise anything, e.g. if a complex - # @property fails). - return UnresolvedAttribute(basename, attr_path, k, val, - e.__class__.__name__, str(e)) - return val - - -class CensoredVariable(): - def __repr__(self): - return "*****" - -class UnresolvedAttribute(): - """ - Container value for failed dot attribute lookups - """ - def __init__(self, basename, attr_path, failure_idx, value, - exc_type, exc_str): - self.basename = basename - self.attr_path = attr_path - self.first_failed = attr_path[failure_idx] - self.failure_idx = failure_idx - self.last_resolvable_value = value - self.exc_type = exc_type - self.exc_str = exc_str - - @property - def last_resolvable_name(self): - return self.basename + '.'.join([''] + self.attr_path[:self.failure_idx]) diff --git a/stackprinter/formatting.py b/stackprinter/formatting.py deleted file mode 100644 index 25f5fd8..0000000 --- a/stackprinter/formatting.py +++ /dev/null @@ -1,251 +0,0 @@ -""" -Various convnenience methods to walk stacks and concatenate formatted frames -""" -import types -import traceback - -import stackprinter.extraction as ex -import stackprinter.colorschemes as colorschemes -from stackprinter.utils import match, get_ansi_tpl -from stackprinter.frame_formatting import FrameFormatter, ColorfulFrameFormatter - - -def get_formatter(style, **kwargs): - if style in ['plaintext', 'plain']: - return FrameFormatter(**kwargs) - else: - return ColorfulFrameFormatter(style, **kwargs) - - -def format_summary(frames, style='plaintext', source_lines=1, reverse=False, - **kwargs): - """ - Render a list of frames with 1 line of source context, no variable values. - - keyword args like stackprinter.format() - """ - min_src_lines = 0 if source_lines == 0 else 1 - minimal_formatter = get_formatter(style=style, - source_lines=min_src_lines, - show_signature=False, - show_vals=False) - - frame_msgs = [minimal_formatter(frame) for frame in frames] - if reverse: - frame_msgs = reversed(frame_msgs) - - return ''.join(frame_msgs) - - -def format_stack(frames, style='plaintext', source_lines=5, - show_signature=True, show_vals='like_source', - truncate_vals=500, line_wrap=60, reverse=False, - suppressed_paths=None, suppressed_vars=[]): - """ - Render a list of frames (or FrameInfo tuples) - - keyword args like stackprinter.format() - """ - - - min_src_lines = 0 if source_lines == 0 else 1 - - minimal_formatter = get_formatter(style=style, - source_lines=min_src_lines, - show_signature=False, - show_vals=False) - - reduced_formatter = get_formatter(style=style, - source_lines=min_src_lines, - show_signature=show_signature, - show_vals=show_vals, - truncate_vals=truncate_vals, - line_wrap=line_wrap, - suppressed_paths=suppressed_paths, - suppressed_vars=suppressed_vars) - - verbose_formatter = get_formatter(style=style, - source_lines=source_lines, - show_signature=show_signature, - show_vals=show_vals, - truncate_vals=truncate_vals, - line_wrap=line_wrap, - suppressed_paths=suppressed_paths, - suppressed_vars=suppressed_vars) - - frame_msgs = [] - parent_is_boring = True - for frame in frames: - fi = ex.get_info(frame, suppressed_vars=suppressed_vars) - is_boring = match(fi.filename, suppressed_paths) - if is_boring: - if parent_is_boring: - formatter = minimal_formatter - else: - formatter = reduced_formatter - else: - formatter = verbose_formatter - - parent_is_boring = is_boring - frame_msgs.append(formatter(fi)) - - if reverse: - frame_msgs = reversed(frame_msgs) - - return ''.join(frame_msgs) - - -def format_stack_from_frame(fr, add_summary=False, **kwargs): - """ - Render a frame and its parents - - - keyword args like stackprinter.format() - - """ - stack = [] - while fr is not None: - stack.append(fr) - fr = fr.f_back - stack = reversed(stack) - - return format_stack(stack, **kwargs) - - -def format_exc_info(etype, evalue, tb, style='plaintext', add_summary='auto', - reverse=False, suppressed_exceptions=[KeyboardInterrupt], - suppressed_vars=[], **kwargs): - """ - Format an exception traceback, including the exception message - - see stackprinter.format() for docs about the keyword arguments - """ - if etype is None: - etype = type(None) - - if etype.__name__ == 'ExceptionGroup': - # Exception groups (new in py 3.11) aren't supported so far, - # but at least we fall back on the default message. - return ''.join(traceback.format_exception(etype, evalue, tb)) - - msg = '' - try: - # First, recursively format any chained exceptions (exceptions - # during whose handling the given one happened). - # TODO: refactor this whole messy function to return a - # more... structured datastructure before assembling a string, - # so that e.g. a summary of the whole chain can be shown at - # the end. - context = getattr(evalue, '__context__', None) - cause = getattr(evalue, '__cause__', None) - suppress_context = getattr(evalue, '__suppress_context__', False) - if cause: - chained_exc = cause - chain_hint = ("\n\nThe above exception was the direct cause " - "of the following exception:\n\n") - elif context and not suppress_context: - chained_exc = context - chain_hint = ("\n\nWhile handling the above exception, " - "another exception occurred:\n\n") - else: - chained_exc = None - - if chained_exc: - msg += format_exc_info(chained_exc.__class__, - chained_exc, - chained_exc.__traceback__, - style=style, - add_summary=add_summary, - reverse=reverse, - suppressed_vars=suppressed_vars, - **kwargs) - - if style == 'plaintext': - msg += chain_hint - else: - sc = getattr(colorschemes, style) - clr = get_ansi_tpl(*sc.colors['exception_type']) - msg += clr % chain_hint - - # Now, actually do some formatting: - parts = [] - if tb: - frameinfos = [ex.get_info(tb_, suppressed_vars=suppressed_vars) - for tb_ in _walk_traceback(tb)] - if (suppressed_exceptions and - issubclass(etype, tuple(suppressed_exceptions))): - summary = format_summary(frameinfos, style=style, - reverse=reverse, **kwargs) - parts = [summary] - else: - whole_stack = format_stack(frameinfos, style=style, - reverse=reverse, **kwargs) - parts.append(whole_stack) - - if add_summary == 'auto': - add_summary = whole_stack.count('\n') > 50 - - if add_summary: - summary = format_summary(frameinfos, style=style, - reverse=reverse, **kwargs) - summary += '\n' - parts.append('---- (full traceback below) ----\n\n' if reverse else - '---- (full traceback above) ----\n') - parts.append(summary) - - exc = format_exception_message(etype, evalue, style=style) - parts.append('\n\n' if reverse else '') - parts.append(exc) - - if reverse: - parts = reversed(parts) - - msg += ''.join(parts) - - except Exception as exc: - import os - if 'PY_STACKPRINTER_DEBUG' in os.environ: - raise - - our_tb = traceback.format_exception(exc.__class__, - exc, - exc.__traceback__, - chain=False) - where = getattr(exc, 'where', None) - context = " while formatting " + str(where) if where else '' - msg = 'Stackprinter failed%s:\n%s\n' % (context, ''.join(our_tb[-2:])) - msg += 'So here is your original traceback at least:\n\n' - msg += ''.join(traceback.format_exception(etype, evalue, tb)) - - - return msg - - -def format_exception_message(etype, evalue, tb=None, style='plaintext'): - type_str = etype.__name__ - val_str = str(evalue) - - if etype == SyntaxError and evalue.text: - val_str += '\n %s\n %s^' % (evalue.text.rstrip(), ' '*evalue.offset) - - if val_str: - type_str += ": " - - if style == 'plaintext': - return type_str + val_str - else: - sc = getattr(colorschemes, style) - - clr_head = get_ansi_tpl(*sc.colors['exception_type']) - clr_msg = get_ansi_tpl(*sc.colors['exception_msg']) - - return clr_head % type_str + clr_msg % val_str - - -def _walk_traceback(tb): - """ - Follow a chain of traceback objects outwards - """ - while tb: - yield tb - tb = tb.tb_next diff --git a/stackprinter/frame_formatting.py b/stackprinter/frame_formatting.py deleted file mode 100644 index 136920e..0000000 --- a/stackprinter/frame_formatting.py +++ /dev/null @@ -1,381 +0,0 @@ -import types -import os - -from collections import OrderedDict -import stackprinter.extraction as ex -import stackprinter.source_inspection as sc -import stackprinter.colorschemes as colorschemes - -from stackprinter.prettyprinting import format_value -from stackprinter.utils import inspect_callable, match, trim_source, get_ansi_tpl - -class FrameFormatter(): - headline_tpl = 'File "%s", line %s, in %s\n' - sourceline_tpl = " %-3s %s" - single_sourceline_tpl = " %s" - marked_sourceline_tpl = "--> %-3s %s" - elipsis_tpl = " (...)\n" - var_indent = 5 - sep_vars = "%s%s" % ((' ') * 4, ('.' * 50)) - sep_source_below = "" - - val_tpl = ' ' * var_indent + "%s = %s\n" - - def __init__(self, source_lines=5, source_lines_after=1, - show_signature=True, show_vals='like_source', - truncate_vals=500, line_wrap: int = 60, - suppressed_paths=None, suppressed_vars=None): - """ - Formatter for single frames. - - This is essentially a partially applied function -- supply config args - to this constructor, then _call_ the resulting object with a frame. - - - Params - --- - source_lines: int or 'all'. (default: 5 lines) - Select how much source code context will be shown. - int 0: Don't include a source listing. - int n > 0: Show n lines of code. - string 'all': Show the whole scope of the frame. - - source_lines_after: int - nr of lines to show after the highlighted one - - show_signature: bool (default True) - Always include the function header in the source code listing. - - show_vals: str or None (default 'like_source') - Select which variable values will be shown. - 'line': Show only the variables on the highlighted line. - 'like_source': Show those visible in the source listing (default). - 'all': Show every variable in the scope of the frame. - None: Don't show any variable values. - - truncate_vals: int (default 500) - Maximum number of characters to be used for each variable value - - line_wrap: int (default 60) - insert linebreaks after this nr of characters, use 0 to never insert - a linebreak - - suppressed_paths: list of regex patterns - Set less verbose formatting for frames whose code lives in certain paths - (e.g. library code). Files whose path matches any of the given regex - patterns will be considered boring. The first call to boring code is - rendered with fewer code lines (but with argument values still visible), - while deeper calls within boring code get a single line and no variable - values. - - Example: To hide numpy internals from the traceback, set - `suppressed_paths=[r"lib/python.*/site-packages/numpy"]` - """ - - - if not (isinstance(source_lines, int) or source_lines == 'all'): - raise ValueError("source_lines must be an integer or 'all', " - "was %r" % source_lines) - - valid_gv = ['all', 'like_source', 'line', None, False] - if show_vals not in valid_gv: - raise ValueError("show_vals must be one of " - "%s, was %r" % (str(valid_gv), show_vals)) - - self.lines = source_lines - self.lines_after = source_lines_after - self.show_signature = show_signature - self.show_vals = show_vals - self.truncate_vals = truncate_vals - self.line_wrap = line_wrap - self.suppressed_paths = suppressed_paths - self.suppressed_vars = suppressed_vars - - def __call__(self, frame, lineno=None): - """ - Render a single stack frame or traceback entry - - - Params - ---- - - frame: Frame object, Traceback object (or FrameInfo tuple) - The frame or traceback entry to be formatted. - - The only difference between passing a frame or a traceback object is - which line gets highlighted in the source listing: For a frame, it's - the currently executed line; for a traceback, it's the line where an - error occurred. (technically: `frame.f_lineno` vs. `tb.tb_lineno`) - - The third option is interesting only if you're planning to format - one frame multiple different ways: It is a little faster to format a - pre-chewed verion of the frame, since non-formatting-specific steps - like "finding the source code", "finding all the variables" etc only - need to be done once per frame. So, this method also accepts the raw - results of `extraction.get_info()` of type FrameInfo. In that case, - this method will really just do formatting, no more chewing. - - lineno: int - override which line gets highlighted - """ - accepted_types = (types.FrameType, types.TracebackType, ex.FrameInfo) - if not isinstance(frame, accepted_types): - raise ValueError("Expected one of these types: " - "%s. Got %r" % (accepted_types, frame)) - - try: - finfo = ex.get_info(frame, lineno, self.suppressed_vars) - - return self._format_frame(finfo) - except Exception as exc: - # If we crash, annotate the exception with the thing - # we were trying to format, for debug/logging purposes. - exc.where = frame - raise - - def _format_frame(self, fi): - msg = self.headline_tpl % (fi.filename, fi.lineno, fi.function) - - source_map, assignments = self.select_scope(fi) - - if source_map: - source_lines = self._format_source(source_map) - msg += self._format_listing(source_lines, fi.lineno) - if assignments: - msg += self._format_assignments(assignments) - elif self.lines == 'all' or self.lines > 1 or self.show_signature: - msg += '\n' - - return msg - - def _format_source(self, source_map): - lines = OrderedDict() - for ln in sorted(source_map): - lines[ln] = ''.join(st for st, _, in source_map[ln]) - return lines - - def _format_listing(self, lines, lineno): - ln_prev = None - msg = "" - n_lines = len(lines) - for ln in sorted(lines): - line = lines[ln] - if ln_prev and ln_prev != ln - 1: - msg += self.elipsis_tpl - ln_prev = ln - - if n_lines > 1: - if ln == lineno: - tpl = self.marked_sourceline_tpl - else: - tpl = self.sourceline_tpl - msg += tpl % (ln, line) - else: - msg += self.single_sourceline_tpl % line - - msg += self.sep_source_below - return msg - - def _format_assignments(self, assignments): - msgs = [] - for name, value in assignments.items(): - val_str = format_value(value, - indent=len(name) + self.var_indent + 3, - truncation=self.truncate_vals, - wrap=self.line_wrap) - assign_str = self.val_tpl % (name, val_str) - msgs.append(assign_str) - if len(msgs) > 0: - return self.sep_vars + '\n' + ''.join(msgs) + self.sep_vars + '\n\n' - else: - return '' - - def select_scope(self, fi): - """ - decide which lines of code and which variables will be visible - """ - source_lines = [] - minl, maxl = 0, 0 - if len(fi.source_map) > 0: - minl, maxl = min(fi.source_map), max(fi.source_map) - lineno = fi.lineno - - if self.lines == 0: - source_lines = [] - elif self.lines == 1: - source_lines = [lineno] - elif self.lines == 'all': - source_lines = range(minl, maxl + 1) - elif self.lines > 1 or self.lines_after > 0: - start = max(lineno - (self.lines - 1), 0) - stop = lineno + self.lines_after - start = max(start, minl) - stop = min(stop, maxl) - source_lines = list(range(start, stop + 1)) - - if source_lines and self.show_signature: - source_lines = sorted(set(source_lines) | set(fi.head_lns)) - - if source_lines: - # Report a bit more info about a weird class of bug - # that I can't reproduce locally. - if not set(source_lines).issubset(fi.source_map.keys()): - debug_vals = [source_lines, fi.head_lns, fi.source_map.keys()] - info = ', '.join(str(p) for p in debug_vals) - raise Exception("Picked an invalid source context: %s" % info) - trimmed_source_map = trim_source(fi.source_map, source_lines) - else: - trimmed_source_map = {} - - if self.show_vals: - if self.show_vals == 'all': - val_lines = range(minl, maxl) - elif self.show_vals == 'like_source': - val_lines = source_lines - elif self.show_vals == 'line': - val_lines = [lineno] if source_lines else [] - - # TODO refactor the whole blacklistling mechanism below: - - def hide(name): - value = fi.assignments[name] - if callable(value): - qualified_name, path, *_ = inspect_callable(value) - is_builtin = value.__class__.__name__ == 'builtin_function_or_method' - is_boring = is_builtin or (qualified_name == name) or (path is None) - is_suppressed = match(path, self.suppressed_paths) - return is_boring or is_suppressed - return False - - visible_vars = (name for ln in val_lines - for name in fi.line2names[ln] - if name in fi.assignments) - - visible_assignments = OrderedDict([(n, fi.assignments[n]) - for n in visible_vars - if not hide(n)]) - else: - visible_assignments = {} - - return trimmed_source_map, visible_assignments - - -class ColorfulFrameFormatter(FrameFormatter): - - def __init__(self, style='darkbg', **kwargs): - """ - See FrameFormatter - this just adds some ANSI color codes here and there - """ - self.colors = getattr(colorschemes, style)() - - highlight = self.tpl('highlight') - header = self.tpl('header') - arrow_lineno = self.tpl('arrow_lineno') - dots = self.tpl('dots') - lineno = self.tpl('lineno') - - self.headline_tpl = header % 'File "%s%s' + highlight % '%s' + header % '", line %s, in %s\n' - self.sourceline_tpl = lineno % super().sourceline_tpl - self.marked_sourceline_tpl = arrow_lineno % super().marked_sourceline_tpl - self.elipsis_tpl = dots % super().elipsis_tpl - self.sep_vars = dots % super().sep_vars - - super().__init__(**kwargs) - - def tpl(self, name): - return get_ansi_tpl(*self.colors[name]) - - def _format_frame(self, fi): - basepath, filename = os.path.split(fi.filename) - sep = os.sep if basepath else '' - msg = self.headline_tpl % (basepath, sep, filename, fi.lineno, fi.function) - source_map, assignments = self.select_scope(fi) - - colormap = self._pick_colors(source_map, fi.name2lines, assignments, fi.lineno) - - if source_map: - source_lines = self._format_source(source_map, colormap, fi.lineno) - msg += self._format_listing(source_lines, fi.lineno) - - if assignments: - msg += self._format_assignments(assignments, colormap) - elif self.lines == 'all' or self.lines > 1 or self.show_signature: - msg += '\n' - - return msg - - def _format_source(self, source_map, colormap, lineno): - bold_tp = self.tpl('source_bold') - default_tpl = self.tpl('source_default') - comment_tpl = self.tpl('source_comment') - - source_lines = OrderedDict() - for ln in source_map: - line = '' - for snippet, ttype in source_map[ln]: - if ttype in [sc.KEYWORD, sc.OP]: - line += bold_tp % snippet - elif ttype == sc.VAR: - if snippet not in colormap: - line += default_tpl % snippet - else: - hue, sat, val, bold = colormap[snippet] - var_tpl = get_ansi_tpl(hue, sat, val, bold) - line += var_tpl % snippet - elif ttype == sc.CALL: - line += bold_tp % snippet - elif ttype == sc.COMMENT: - line += comment_tpl % snippet - else: - line += default_tpl % snippet - source_lines[ln] = line - - return source_lines - - def _format_assignments(self, assignments, colormap): - msgs = [] - for name, value in assignments.items(): - val_str = format_value(value, - indent=len(name) + self.var_indent + 3, - truncation=self.truncate_vals, - wrap=self.line_wrap) - assign_str = self.val_tpl % (name, val_str) - hue, sat, val, bold = colormap.get(name, self.colors['var_invisible']) - clr_str = get_ansi_tpl(hue, sat, val, bold) % assign_str - msgs.append(clr_str) - if len(msgs) > 0: - return self.sep_vars + '\n' + ''.join(msgs) + self.sep_vars + '\n\n' - else: - return '' - - def _pick_colors(self, source_map, name2lines, assignments, lineno): - # TODO refactor: pick a hash for each name across frames, _then_ color. - # Currently, colors are consistent across frames purely because there's - # a fixed map from hashes to colors. It's not bijective though. If colors - # were picked after hashing across all frames, that could be fixed. - colormap = {} - for line in source_map.values(): - for name, ttype in line: - if name not in colormap and ttype == sc.VAR and name in assignments: - value = assignments[name] - highlight = lineno in name2lines[name] - colormap[name] = self._pick_color(name, value, highlight) - return colormap - - def _pick_color(self, name, val, highlight=False, method='id'): - if method == 'formatted': - seed = format_value(val) - elif method == 'repr': - seed = repr(val) - elif method == 'id': - seed = id(val) - elif method == 'name': - seed = name - else: - raise ValueError('%r' % method) - - return self.colors.get_random(seed, highlight) - - - - diff --git a/stackprinter/prettyprinting.py b/stackprinter/prettyprinting.py deleted file mode 100644 index 6e17f97..0000000 --- a/stackprinter/prettyprinting.py +++ /dev/null @@ -1,259 +0,0 @@ -import os - -from stackprinter.extraction import UnresolvedAttribute -from stackprinter.utils import inspect_callable - -try: - import numpy as np -except ImportError: - np = False - -MAXLEN_DICT_KEY_REPR = 25 # truncate dict keys to this nr of characters - -# TODO see where the builtin pprint module can be used instead of all this -# (but how to extend it for e.g. custom np array printing?) - -def format_value(value, indent=0, truncation=None, wrap=60, - max_depth=2, depth=0): - """ - Stringify some object - - Params - --- - value: object to be formatted - - indent: int - insert extra spaces on each line - - truncation: int - cut after this nr of characters - - wrap: int - insert linebreaks after this nr of characters, use 0 to never add a linebreak - - max_depth: int - max repeated calls to this function when formatting container types - - depth: int - current nesting level - - Returns - --- - string - """ - - if depth > max_depth: - return '...' - - if isinstance(value, UnresolvedAttribute): - reason = "# %s" % (value.exc_type) - val_tpl = reason + "\n%s = %s" - lastval_str = format_value(value.last_resolvable_value, - truncation=truncation, indent=3, depth=depth+1) - val_str = val_tpl % (value.last_resolvable_name, lastval_str) - indent = 10 - - elif isinstance(value, (list, tuple, set)): - val_str = format_iterable(value, truncation, max_depth, depth) - - elif isinstance(value, dict): - val_str = format_dict(value, truncation, max_depth, depth) - - elif np and isinstance(value, np.ndarray): - val_str = format_array(value, minimize=depth > 0) - - elif callable(value): - name, filepath, method_owner, ln = inspect_callable(value) - filename = os.path.basename(filepath) if filepath is not None else None - if filename is None: - val_str = safe_repr(value) - elif method_owner is None: - name_s = safe_str(name) - filename_s = safe_str(filename) - ln_s = safe_str(ln) - val_str = "" % (name_s, filename_s, ln_s) - else: - name_s = safe_str(name) - filename_s = safe_str(filename) - method_owner_s = safe_str(method_owner) - ln_s = safe_str(ln) - val_str = "" % (name_s, method_owner_s, - filename_s, ln_s) - else: - val_str= safe_repr_or_str(value) - - val_str = truncate(val_str, truncation) - - if depth == 0: - val_str = wrap_lines(val_str, wrap) - - if indent > 0: - nl_indented = '\n' + (' ' * indent) - val_str = val_str.replace('\n', nl_indented) - - return val_str - - -def format_dict(value, truncation, max_depth, depth): - typename = value.__class__.__name__ - prefix = '{' if type(value) == dict else "%s\n{" % typename - postfix = '}' - - if depth == max_depth: - val_str = '...' - else: - vstrs = [] - char_count = 0 - for k, v in value.items(): - if char_count >= truncation: - break - kstr = truncate(repr(k), MAXLEN_DICT_KEY_REPR) - vstr = format_value(v, indent=len(kstr) + 3, - truncation=truncation, depth=depth+1) - istr = "%s: %s" % (kstr, vstr) - vstrs.append(istr) - char_count += len(istr) - - - val_str = ',\n '.join(vstrs) - - return prefix + val_str + postfix - - -def format_iterable(value, truncation, max_depth, depth): - typename = value.__class__.__name__ - if isinstance(value, list): - prefix = '[' if type(value) == list else "%s [" % typename - postfix = ']' - - elif isinstance(value, tuple): - prefix = '(' if type(value) == tuple else "%s (" % typename - postfix = ')' - - elif isinstance(value, set): - prefix = '{' if type(value) == set else "%s {" % typename - postfix = '}' - - - length = len(value) - val_str = '' - if depth == max_depth: - val_str += '...' - else: - linebreak = False - char_count = 0 - for i,v in enumerate(value): - if char_count >= truncation: - val_str += "..." - break - item_str = '' - entry = format_value(v, indent=1, truncation=truncation, - depth=depth+1) - sep = ', ' if i < length else '' - if '\n' in entry: - item_str += "\n %s%s" % (entry, sep) - linebreak = True - else: - if linebreak: - item_str += '\n' - linebreak = False - item_str += "%s%s" % (entry, sep) - val_str += item_str - char_count += len(item_str) - - return prefix + val_str + postfix - - -def format_array(arr, minimize=False): - """ - format a numpy array (with shape information) - - Params - --- - minimize: bool - use an extra compact oneline format - """ - if arr.ndim >= 1: - shape = list(arr.shape) - if len(shape) < 2: - shape.append('') - shape_str = "x".join(str(d) for d in shape) - if len(shape_str) < 10: - prefix = "%s array(" % shape_str - msg = prefix - else: - prefix = "" - msg = "%s array(\n" % shape_str - else: - msg = prefix = "array(" - - suffix = ')' - - try: - array_rep = np.array2string(arr, max_line_width=9000, threshold=50, - edgeitems=2, prefix=prefix, suffix=suffix) - except TypeError: - # some super old numpy versions (< 1.14) don't accept all these arguments - array_rep = np.array2string(arr, max_line_width=9000, prefix=prefix) - - if minimize and (len(array_rep) > 50 or arr.ndim > 1): - array_rep = "%s%s...%s" % ('[' * arr.ndim, arr.flatten()[0], ']' * arr.ndim) - - - msg += array_rep + suffix - return msg - -def safe_repr(value): - try: - return repr(value) - except: - return '# error calling repr' - - -def safe_str(value): - try: - return str(value) - except: - return '# error calling str' - - -def safe_repr_or_str(value): - try: - return repr(value) - except: - try: - return str(value) - except: - return '# error calling repr and str' - - -def truncate(string, n): - if not n: - return string - n = max(n, 0) - if len(string) > (n+3): - string = "%s..." % string[:n].rstrip() - return string - - -def wrap_lines(string, max_width=80): - if not max_width or max_width <= 0: - return string - - def wrap(lines): - for l in lines: - length = len(l) - if length <= max_width: - yield l - else: - k = 0 - while k < length: - snippet = l[k:k+max_width] - if k > 0: - snippet = " " + snippet - - yield snippet - k += max_width - - wrapped_lines = wrap(string.splitlines()) - return '\n'.join(wrapped_lines) diff --git a/stackprinter/source_inspection.py b/stackprinter/source_inspection.py deleted file mode 100644 index 63bc52c..0000000 --- a/stackprinter/source_inspection.py +++ /dev/null @@ -1,251 +0,0 @@ -import tokenize -import warnings -from keyword import kwlist -from collections import defaultdict - -RAW = 'RAW' -COMMENT = 'COMM' -VAR = 'VAR' -KEYWORD = 'KW' -CALL = 'CALL' -OP = 'OP' - - -def annotate(source_lines, line_offset=0, lineno=0, max_line=2**15): - """ - Find out where in a piece of code which variables live. - - This tokenizes the source, maps out where the variables occur, and, weirdly, - collapses any multiline continuations (i.e. lines ending with a backslash). - - - Params - --- - line_offset: int - line number of the first element of source_lines in the original file - - lineno: int - A line number you're especially interested in. If this line moves around - while treating multiline statements, the corrected nr will be returned. - Otherwise, the given nr will be returned. - - max_line: int - Stop analysing after this many lines - - Returns - --- - source_map: OrderedDict - Maps line numbers to a list of tokens. Each token is a (string, TYPE) - tuple. Concatenating the first elements of all tokens of all lines - restores the original source, weird whitespaces/indentations and all - (in contrast to python's built-in `tokenize`). However, multiline - statements (those with a trailing backslash) are secretly collapsed - into their first line. - - line2names: dict - Maps each line number to a list of variables names that occur there - - name2lines: dict - Maps each variable name to a list of line numbers where it occurs - - head_lns: (int, int) or (None, None) - Line numbers of the beginning and end of the function header - - lineno: int - identical to the supplied argument lineno, unless that line had to be - moved when collapsing a backslash-continued multiline statement. - """ - if not source_lines: - return {}, {}, {}, [], lineno - - assert isinstance(line_offset, int) - assert isinstance(lineno, int) - assert isinstance(max_line, int) - - source_lines, lineno_corrections = join_broken_lines(source_lines) - lineno += lineno_corrections[lineno - line_offset] - - max_line_relative = min(len(source_lines), max_line-line_offset) - tokens, head_s, head_e = _tokenize(source_lines[:max_line_relative]) - - tokens_by_line = defaultdict(list) - name2lines = defaultdict(list) - line2names = defaultdict(list) - for ttype, string, (sline, scol), (eline, ecol) in tokens: - ln = sline + line_offset - tokens_by_line[ln].append((ttype, scol, ecol, string)) - if ttype == VAR: - name2lines[string].append(ln) - line2names[ln].append(string) - - source_map = {} - for ln, line in enumerate(source_lines): - ln = ln + line_offset - regions = [] - col = 0 - for ttype, tok_start, tok_end, string in tokens_by_line[ln]: - if tok_start > col: - snippet = line[col:tok_start] - regions.append((snippet, RAW)) - col = tok_start - snippet = line[tok_start:tok_end] - if snippet != string: - msg = ("Token %r doesn't match raw source %r" - " in line %s: %r" % (string, snippet, ln, line)) - warnings.warn(msg) - regions.append((snippet, ttype)) - col = tok_end - - if col < len(line): - snippet = line[col:] - regions.append((snippet, RAW)) - - source_map[ln] = regions - - if head_s is not None and head_e is not None: - head_lines = list(range(head_s + line_offset, 1 + head_e + line_offset)) - else: - head_lines = [] - - return source_map, line2names, name2lines, head_lines, lineno - - -def _tokenize(source_lines): - """ - Split a list of source lines into tokens - - Params - --- - source_lines: list of str - - Returns - --- - - list of tokens, each a list of this format: - [TOKENTYPE, 'string', (startline, startcolumn), (endline, endcol)] - - """ - - tokenizer = tokenize.generate_tokens(iter(source_lines).__next__) - # Dragons! This is a trick from the `inspect` standard lib module: Using the - # undocumented method generate_tokens() instead of the official tokenize(), - # since the latter doesn't accept strings (only `readline()`s). The official - # way would be to repackage our list of strings, something like this.. :( - # source = "".join(source_lines) - # source_bytes = BytesIO(source.encode('utf-8')).readline - # tokenizer = tokenize.tokenize(source_bytes) - - tokens = [] - - dot_continuation = False - was_name = False - open_parens = 0 - - head_s = None - head_e = None - name_end = -2 - acceptable_multiline_tokens = [tokenize.STRING] - if hasattr(tokenize, "FSTRING_START"): - # we're >= python 3.12 - acceptable_multiline_tokens.extend([ - tokenize.FSTRING_START, - tokenize.FSTRING_MIDDLE, - tokenize.FSTRING_END]) - - for ttype, string, (sline, scol), (eline, ecol), line in tokenizer: - sline -= 1 # we deal in line indices counting from 0 - eline -= 1 - - if ttype not in acceptable_multiline_tokens: - assert sline == eline, "Can't accept non-string multiline tokens" - - if ttype == tokenize.NAME: - if string in kwlist: - tokens.append([KEYWORD, string, (sline, scol), (eline, ecol)]) - if head_s is None and string == 'def': - # while we're here, note the start of the call signature - head_s = sline - - elif not dot_continuation: - tokens.append([VAR, string, (sline, scol), (eline, ecol)]) - else: - # this name seems to be part of an attribute lookup, - # which we want to treat as one long name. - prev = tokens[-1] - extended_name = prev[1] + "." + string - old_eline, old_ecol = prev[3] - end_line = max(old_eline, eline) - end_col = max(old_ecol, ecol) - tokens[-1] = [VAR, extended_name, prev[2], (end_line, end_col)] - dot_continuation = False - was_name = True - name_end = ecol - 1 - else: - if string == '.' and was_name and scol == name_end + 1: - dot_continuation = True - continue - elif string == '(': - open_parens += 1 - elif string == ')': - # while we're here, note the end of the call signature. - # the parens counting is necessary because default keyword - # args can contain '(', ')', e.g. in object instantiations. - open_parens -= 1 - if head_e is None and open_parens == 0 and head_s is not None: - head_e = sline - - if ttype == tokenize.OP: - tokens.append([OP, string, (sline, scol), (eline, ecol)]) - if ttype == tokenize.COMMENT: - tokens.append([COMMENT, string, (sline, scol), (eline, ecol)]) - was_name = False - name_end = -2 - - # TODO: proper handling of keyword argument assignments: left hand sides - # should be treated as variables _only_ in the header of the current - # function, and outside of calls, but not when calling other functions... - # this is getting silly. - return tokens, head_s, head_e - - -def join_broken_lines(source_lines): - """ - Collapse backslash-continued lines into the first (upper) line - """ - - # TODO meditate whether this is a good idea - - n_lines = len(source_lines) - unbroken_lines = [] - k = 0 - lineno_corrections = defaultdict(lambda: 0) - while k < n_lines: - line = source_lines[k] - - gobbled_lines = [] - while (line.endswith('\\\n') - and k + 1 < n_lines - and line.lstrip()[0] != '#'): - k_continued = k - k += 1 - nextline = source_lines[k] - nextline_stripped = nextline.lstrip() - line = line[:-2] + nextline_stripped - - indent = '' - n_raw, n_stripped = len(nextline), len(nextline_stripped) - if n_raw != n_stripped: - white_char = nextline[0] - fudge = 3 if white_char == ' ' else 0 - indent = white_char * max(0, (n_raw - n_stripped - fudge)) - - gobbled_lines.append(indent + "\n" ) - lineno_corrections[k] = k_continued - k - - unbroken_lines.append(line) - unbroken_lines.extend(gobbled_lines) - k += 1 - - return unbroken_lines, lineno_corrections - - diff --git a/stackprinter/tracing.py b/stackprinter/tracing.py deleted file mode 100644 index f6a274c..0000000 --- a/stackprinter/tracing.py +++ /dev/null @@ -1,194 +0,0 @@ -import sys -import inspect - -from stackprinter.frame_formatting import FrameFormatter, ColorfulFrameFormatter -from stackprinter.formatting import format_exception_message, get_formatter -from stackprinter import prettyprinting as ppr -from stackprinter.utils import match - - -def trace(*args, suppressed_paths=[], **formatter_kwargs): - """ - Get a decorator to print all calls & returns in a function - - Example: - ``` - @trace(style='color', depth_limit=5) - def dosometing(): - (...) - ``` - - Params - --- - Accepts all keyword wargs accepted by stackprinter.format, and: - - depth_limit: int (default: 20) - How many nested calls will be followed - - print_function: callable (default: print) - some function of your choice that accepts a string - - stop_on_exception: bool (default: True) - If False, plow through exceptions - - - """ - traceprinter = TracePrinter(suppressed_paths=suppressed_paths, - **formatter_kwargs) - - def deco(f): - def wrapper(*args, **formatter_kwargs): - traceprinter.enable(current_depth=count_stack(sys._getframe()) + 1) - result = f(*args, **formatter_kwargs) - traceprinter.disable() - return result - return wrapper - - if args: - return deco(args[0]) - else: - return deco - - -class TracePrinter(): - """ - Print a trace of all calls & returns in a piece of code as they are executed - - Example: - ``` - with Traceprinter(style='color', depth_limit=5): - dosomething() - dosomethingelse() - ``` - - Params - --- - Accepts all keyword wargs accepted by stackprinter.format, and: - - depth_limit: int (default: 20) - How many nested calls will be followed - - print_function: callable (default: print) - some function of your choice that accepts a string - - stop_on_exception: bool (default: True) - If False, plow through exceptions - - """ - - def __init__(self, - suppressed_paths=[], - depth_limit=20, - print_function=print, - stop_on_exception=True, - **formatter_kwargs): - - self.fmt = get_formatter(**formatter_kwargs) - self.fmt_style = formatter_kwargs.get('style', 'plaintext') - assert isinstance(suppressed_paths, list) - self.suppressed_paths = suppressed_paths - self.emit = print_function - self.depth_limit = depth_limit - self.stop_on_exception = stop_on_exception - - def __enter__(self): - depth = count_stack(sys._getframe(1)) - self.enable(current_depth=depth) - return self - - def __exit__(self, etype, evalue, tb): - self.disable() - if etype is None: - return True - - - def enable(self, force=False, current_depth=None): - if current_depth is None: - current_depth = count_stack(sys._getframe(1)) - self.starting_depth = current_depth - self.previous_frame = None - self.trace_before = sys.gettrace() - if (self.trace_before is not None) and not force: - raise Exception("There is already a trace function registered: %r" % self.trace_before) - sys.settrace(self.trace) - - def disable(self): - sys.settrace(self.trace_before) - try: - del self.previous_frame - except AttributeError: - pass - - def trace(self, frame, event, arg): - depth = count_stack(frame) - self.starting_depth - if depth >= self.depth_limit: - return None - - if 'call' in event: - callsite = frame.f_back - self.show(callsite) - self.show(frame) - elif 'return' in event: - val_str = ppr.format_value(arg, indent=11, truncation=1000) - ret_str = ' Return %s\n' % val_str - self.show(frame, note=ret_str) - elif event == 'exception': - exc_str = format_exception_message(*arg, style=self.fmt_style) - self.show(frame, note=exc_str) - if self.stop_on_exception: - self.disable() - return None - - return self.trace - - def show(self, frame, note=''): - if frame is None: - return - - filepath = inspect.getsourcefile(frame) or inspect.getfile(frame) - if filepath in __file__: - return - elif match(filepath, self.suppressed_paths): - line_info = (filepath, frame.f_lineno, frame.f_code.co_name) - frame_str = 'File %s, line %s, in %s\n' % line_info - if len(note) > 123: - note == note[:120] + '...' - else: - frame_str = self.fmt(frame) - - depth = count_stack(frame) - self.starting_depth - our_callsite = frame.f_back - callsite_of_previous_frame = getattr(self.previous_frame, 'f_back', -1) - if self.previous_frame is our_callsite and our_callsite is not None: - # we're a child frame - self.emit(add_indent(' └──┐\n', depth - 1)) - if frame is callsite_of_previous_frame: - # we're a parent frame - self.emit(add_indent('β”Œβ”€β”€β”€β”€β”€β”€β”˜\n', depth)) - - - frame_str += note - self.emit(add_indent(frame_str, depth)) - self.previous_frame = frame - - -def add_indent(string, depth=1, max_depth=10): - depth = max(depth, 0) - - if depth > max_depth: - indent = '%s ' % depth + ' ' * (depth % max_depth) - else: - indent = ' ' * depth - - lines = [indent + line + '\n' for line in string.splitlines()] - indented = ''.join(lines) - return indented - - -def count_stack(frame): - depth = 1 - fr = frame - while fr.f_back: - fr = fr.f_back - depth += 1 - return depth diff --git a/stackprinter/utils.py b/stackprinter/utils.py deleted file mode 100644 index bb5968b..0000000 --- a/stackprinter/utils.py +++ /dev/null @@ -1,100 +0,0 @@ -import re -import types -import inspect -import colorsys -from collections import OrderedDict - - -def match(string, patterns): - if patterns is None or not isinstance(string, str): - return False - if isinstance(patterns, str): - patterns = [patterns] - - return any(map(lambda p: re.search(p, string), patterns)) - - -def inspect_callable(f): - """ - Find out to which object & file a function belongs - """ - # TODO cleanup - - owner = getattr(f, '__self__', None) - - if inspect.ismethod(f): - f = f.__func__ - - if inspect.isfunction(f): - code = f.__code__ - # elif isinstance(f, types.BuiltinFunctionType): - # ? - else: - return None, None, None, None - - qname = getattr(f, '__qualname__', None) - - # under pypy, builtin code object (like: [].append.__func__.__code__) - # have no co_filename and co_firstlineno - filepath = getattr(code, 'co_filename', None) - ln = getattr(code, 'co_firstlineno', None) - - return qname, filepath, owner, ln - - -def trim_source(source_map, context): - """ - get part of a source listing, with extraneous indentation removed - - """ - indent_type = None - min_indent = 9000 - for ln in context: - (snippet0, *meta0), *remaining_line = source_map[ln] - - if snippet0.startswith('\t'): - if indent_type == ' ': - # Mixed tabs and spaces - not trimming whitespace. - return source_map - else: - indent_type = '\t' - elif snippet0.startswith(' '): - if indent_type == '\t': - # Mixed tabs and spaces - not trimming whitespace. - return source_map - else: - indent_type = ' ' - elif snippet0.startswith('\n'): - continue - - n_nonwhite = len(snippet0.lstrip(' \t')) - indent = len(snippet0) - n_nonwhite - min_indent = min(indent, min_indent) - - trimmed_source_map = OrderedDict() - for ln in context: - (snippet0, *meta0), *remaining_line = source_map[ln] - if not snippet0.startswith('\n'): - snippet0 = snippet0[min_indent:] - trimmed_source_map[ln] = [[snippet0] + meta0] + remaining_line - - return trimmed_source_map - - - -def get_ansi_tpl(hue, sat, val, bold=False): - - # r_, g_, b_ = colorsys.hls_to_rgb(hue, val, sat) - r_, g_, b_ = colorsys.hsv_to_rgb(hue, sat, val) - r = int(round(r_*5)) - g = int(round(g_*5)) - b = int(round(b_*5)) - point = 16 + 36 * r + 6 * g + b - # print(r,g,b,point) - - bold_tp = '1;' if bold else '' - code_tpl = ('\u001b[%s38;5;%dm' % (bold_tp, point)) + '%s\u001b[0m' - return code_tpl - - - diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 572014a..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -import os -os.environ['PY_STACKPRINTER_DEBUG'] = '1' - diff --git a/tests/source.py b/tests/source.py deleted file mode 100644 index 64d4b1e..0000000 --- a/tests/source.py +++ /dev/null @@ -1,97 +0,0 @@ -import numpy as np -import pytest - - -def spam(thing, - various=None, - other=None, - things=None): - """ - some very long - doc string - """ - bla = """ another triple quoted string """ - X = np.zeros((10,10)) - X[0,0] = 1 - boing = outer_scope_thing['and'] - foo = somelist - do_something = lambda val: spam_spam_spam(val + outer_scope_thing['and'] + boing) - for k in X: - if np.sum(k) != 0: - return [[do_something(val) for val in row] for row in X] - - -class Hovercraft(): - @property - def eels(self): - try: - return spam_spam('') - except Exception as e: - raise Exception('ahoi!') from e - - -blub = '🐍' -somelist = [1,2,3,np.zeros((23,42)), np.ones((42,23))] -outer_scope_thing = {'various': 'things', - 123: 'in a dict', - 'and': np.ones((42,23)), - 'in a list': somelist} - -def deco(f): - bla = '🐍🐍🐍' - def closure(*args, **kwargs): - kwargs['zap'] = bla - return f(*args, **kwargs) - - return closure - -@deco -def spam_spam(boing, zap='!'): - thing = boing + zap - spam(thing) - - -def spam_spam_spam(val): - if np.sum(val) != 42: - bla = val.T.\ - T.T.\ - T - - # here is a comment \ - # that has backslashes \ - # for some reason - eels = "here is a "\ - "multi line "\ - "string" - - foo = f"""here is - {outer_scope_thing["and"]} - a multi-line f-string, - even with quotes in it - """ - # np.reshape(bla, 9000) - try: - bla.nonexistant_attribute - except: - pass - supersecret = "you haven't seen this" - boing = np.\ - random.rand(*bla.T.\ - T.T) - raise Exception('something happened') - -#### - -@pytest.fixture -def sourcelines(): - with open(__file__, 'r') as sf: - lines = sf.readlines() - return lines - - -if __name__ == '__main__': - import stackprinter - try: - Hovercraft().eels - except: - stackprinter.show(style='darkbg') \ No newline at end of file diff --git a/tests/test_formatting.py b/tests/test_formatting.py deleted file mode 100644 index 8ff52c4..0000000 --- a/tests/test_formatting.py +++ /dev/null @@ -1,90 +0,0 @@ -import stackprinter - - -def test_frame_formatting(): - """ pin plaintext output """ - msg = stackprinter.format() - lines = msg.split('\n') - - expected = ['File "test_formatting.py", line 6, in test_frame_formatting', - ' 4 def test_frame_formatting():', - ' 5 """ pin plaintext output """', - '--> 6 msg = stackprinter.format()', - " 7 lines = msg.split('\\n')", - ' ..................................................', - " stackprinter.format = ", - ' ..................................................', - '', - ''] - - for k, (our_line, expected_line) in enumerate(zip(lines[-len(expected):], expected)): - if k == 0: - assert our_line[-52:] == expected_line[-52:] - elif k == 6: - assert our_line[:58] == expected_line[:58] - else: - assert our_line == expected_line - - # for scheme in stackprinter.colorschemes.__all__: - # stackprinter.format(style=scheme, suppressed_paths=[r"lib/python.*"]) - - -def test_exception_formatting(): - from source import Hovercraft - - try: - Hovercraft().eels - except: - msg_plain = stackprinter.format() - msg_color = stackprinter.format(style='darkbg') - - lines = msg_plain.split('\n') - - assert lines[0].endswith('eels') - assert lines[-1] == 'Exception: ahoi!' - - print(msg_plain) - print(msg_color) - - -def test_none_tuple_formatting(): - output = stackprinter.format((None, None, None)) - assert output == "NoneType: None" - - -def test_none_value_formatting(): - output = stackprinter.format((TypeError, None, None)) - assert output == "TypeError: None" - - -def test_tb_with_none_lineno(): - # Regression Python 3.12+ compat. - # Since CPython 3.12, `tb.tb_lineno` (and `frame.f_lineno`) return None when - # the instruction at tb_lasti has no line mapping. Stackprinter used to pass - # that None straight into source_inspection.annotate() and blow up on - # `assert isinstance(lineno, int)`. It should now fall back gracefully. - import sys - import types - - import pytest - - captured = [] - - def target(): - captured.append(sys._getframe()) - - target() - frame = captured[0] - - # An out-of-range tb_lasti makes CPython's tb_lineno getter return None via - # its Py_RETURN_NONE fallback path. - tb = types.TracebackType(None, frame, 1 << 20, -1) - if tb.tb_lineno is not None: - pytest.skip( - "tb.tb_lineno didn't return None on this interpreter " - f"({sys.version_info}); regression only applies when it does." - ) - - output = stackprinter.format((ValueError, ValueError("boom"), tb)) - assert "target" in output - assert "ValueError: boom" in output diff --git a/tests/test_frame_inspection.py b/tests/test_frame_inspection.py deleted file mode 100644 index 0a44095..0000000 --- a/tests/test_frame_inspection.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys -import pytest -import stackprinter.extraction as ex - -@pytest.fixture -def frameinfo(): - somevalue = 'spam' - - supersecretthings = "gaaaah" - class Blob(): - pass - someobject = Blob() - someobject.secretattribute = "uarrgh" - - fr = sys._getframe() - hidden = [r".*secret.*", r"someobject\..*attribute"] - return ex.get_info(fr, suppressed_vars=hidden) - -def test_frameinfo(frameinfo): - fi = frameinfo - assert fi.filename.endswith('test_frame_inspection.py') - assert fi.function == 'frameinfo' - assert fi.assignments['somevalue'] == 'spam' - assert isinstance(fi.assignments['supersecretthings'], ex.CensoredVariable) - assert isinstance(fi.assignments['someobject.secretattribute'], ex.CensoredVariable) diff --git a/tests/test_source_inspection.py b/tests/test_source_inspection.py deleted file mode 100644 index 5449fb2..0000000 --- a/tests/test_source_inspection.py +++ /dev/null @@ -1,35 +0,0 @@ -from source import sourcelines -from stackprinter import source_inspection as si - - -def test_source_annotation(sourcelines): - """ """ - line_offset = 23 - source_map, line2names, name2lines, head_lns, lineno = si.annotate(sourcelines, line_offset, 42) - - # see that we didn't forget or invent any lines - assert len(source_map) == len(sourcelines) - - # reassemble the original source, whitespace and all - # (except when we hit the `\`-line continuations at the bottom of the - # file - parsing collapses continued lines, so those can't be reconstructed.) - for k, (true_line, parsed_line) in enumerate(zip(sourcelines, source_map.values())): - if '\\' in true_line: - assert k >= 50 - break - reconstructed_line = ''.join([snippet for snippet, ttype in parsed_line]) - assert reconstructed_line == true_line - - - # see if we found this known token - assert source_map[17 + line_offset][5] == ('lambda', 'KW') - - # see if we found this name - assert "spam_spam_spam" in line2names[17 + line_offset] - assert 17 + line_offset in name2lines["spam_spam_spam"] - - # see if we found this multiline function header - assert head_lns == [k + line_offset for k in [4,5,6,7]] - - # ... and that lineno survived the roundtrip - assert lineno == 42 \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index feb6445..0000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -import re - -from stackprinter.utils import match - - -def test_match(): - assert match('my/ignored/path', None) is False - assert match('my/ignored/path', ['not']) is False - assert match('my/ignored/path', [re.compile('not ignored')]) is False - - assert match('my/ignored/path', 'ignored') - assert match('my/ignored/path', ['not', 'ignored']) - assert match('my/ignored/path', [re.compile('not ignored'), re.compile('ignored')]) diff --git a/trace.png b/trace.png deleted file mode 100644 index f24031a..0000000 Binary files a/trace.png and /dev/null differ diff --git a/tracebacks.py b/tracebacks.py new file mode 100644 index 0000000..88c9eae --- /dev/null +++ b/tracebacks.py @@ -0,0 +1,143 @@ +import sys +import traceback +import inspect + + +def printlocals(frame, truncate=500, truncate__=True): + sep = '.' * 50 + msg = ' %s\n' % sep + for name, value in sorted(frame.f_locals.items()): + if hasattr(value, '__repr__'): + try: + val_str = value.__repr__() + except: + val_str = "" + else: + try: + val_str = str(value) + except: + val_str = "" + + if truncate and len(val_str) > truncate: + val_str = "%s..." % val_str[:truncate] + if truncate__ and name.startswith('__') and len(val_str) > 50: + val_str = "%s..." % val_str[:50] + val_str = val_str.replace('\n', '\n %s' % (' ' * (len(name) + 2))) + + msg += " %s = %s\n" % (name, val_str) + + msg += ' %s' % sep + return msg + + +def get_lines(frame): + """ + get source for this frame + + Params + --- + frame : frame object + + Returns + --- + lines : list of str + + startline : int + line number of lines[0] in the original source file + """ + if frame.f_code.co_name == '': + lines, _ = inspect.findsource(frame) + startline = 1 + else: + lines, startline = inspect.getsourcelines(frame) + + return lines, startline + + +def format_line(line, line_idx, lineno=None): + if lineno is not None and line_idx == lineno: + msg = "--> %-3s %s" % (line_idx, line) + else: + msg = " %-3s %s" % (line_idx, line) + return msg + + +def get_source_lines(frame, lineno, context=5, always_show_signature=True): + try: + lines, startline = get_lines(frame) + except: + return '' + + name = frame.f_code.co_name + start = max(lineno - context, 0) + stop = lineno + 1 + source_str = '' + if start > startline and always_show_signature and name != '': + source_str += format_line(lines[0], startline) + source_str += ' (...)\n' + + for ln, line in enumerate(lines): + line_idx = ln + startline + if line_idx >= start and line_idx <= stop: + source_str += format_line(line, line_idx, lineno) + return source_str + + +def tb2string(tb, ): + frame_strings = [] + while tb: + frame = tb.tb_frame + lineno = tb.tb_lineno + frameinfo = inspect.getframeinfo(frame) + msg = "File %s, line %s in %s\n" % (frameinfo.filename, lineno, frameinfo.function) + + source_str = get_source_lines(frame, lineno) + if source_str: + msg += source_str + msg += printlocals(frame) + + + frame_strings.append(msg) + tb = tb.tb_next + + return '\n\n\n'.join(frame_strings) + + +def format_traceback(tb, etype=None, evalue=None): + msg = tb2string(tb) + if etype is not None and evalue is not None: + exc_str = ' '.join(traceback.format_exception_only(etype, evalue)) + msg += '\n' + exc_str + return msg + + + +if __name__ == '__main__': + import numpy as np + + def a_broken_function(thing, otherthing=1234): + # very long function + # with many lines + # and various weird variables + X = np.zeros((5, 5)) + X[0] = len(thing) + for k in X: + if np.sum(k) != 0: + raise Exception('something happened') + # more code: + zup = 123 + + def some_function(boing, zap='!'): + thing = boing + zap + a_broken_function(thing) + + # some_function("hello") + try: + some_function("hello") + except: + etype, exc, tb = sys.exc_info() + print(format_traceback(tb, etype, exc)) + + + +