beautiful-traceback 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.3
2
+ Name: beautiful-traceback
3
+ Version: 0.1.0
4
+ Summary: Beautiful, readable Python tracebacks with colors and formatting
5
+ Keywords: traceback,error,debugging,formatting
6
+ Author: Michael Bianco
7
+ Author-email: Michael Bianco <mike@mikebian.co>
8
+ Requires-Dist: colorama>=0.4.6
9
+ Requires-Python: >=3.9
10
+ Project-URL: Repository, https://github.com/iloveitaly/beautiful-traceback
11
+ Description-Content-Type: text/markdown
12
+
13
+ # Beautiful Traceback
14
+
15
+ > **Note:** This is a fork of the [pretty-traceback](https://github.com/mbarkhau/pretty-traceback) repo with simplified development and improvements for better integration with FastAPI, [structlog](https://github.com/iloveitaly/structlog-config), IPython, pytest, and more. This project is used in [python-starter-template](https://github.com/iloveitaly/python-starter-template) to provide better debugging experience in production environments.
16
+
17
+ Human readable stacktraces for Python.
18
+
19
+ [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
20
+ [![Python Versions](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
21
+
22
+ ## Quick Start
23
+
24
+ The fastest way to see it in action:
25
+
26
+ ```bash
27
+ # Clone and run an example
28
+ git clone https://github.com/iloveitaly/beautiful-traceback
29
+ cd beautiful-traceback
30
+ uv run examples/simple.py
31
+ ```
32
+
33
+ ## Overview
34
+
35
+ Beautiful Traceback groups together what belongs together, adds coloring and alignment. All of this makes it easier for you to see patterns and filter out the signal from the noise. This tabular format is best viewed in a wide terminal.
36
+
37
+ ![Comparison of standard Python traceback vs Beautiful Traceback](comparison.webp)
38
+
39
+ ## Installation
40
+
41
+ ### From PyPI (when published)
42
+
43
+ ```bash
44
+ # Using uv (recommended)
45
+ uv add beautiful-traceback
46
+
47
+ # Using pip
48
+ pip install beautiful-traceback
49
+ ```
50
+
51
+ ### Development Installation
52
+
53
+ To install from source:
54
+
55
+ ```bash
56
+ git clone https://github.com/iloveitaly/beautiful-traceback
57
+ cd beautiful-traceback
58
+ uv sync
59
+ ```
60
+
61
+ Run examples:
62
+ ```bash
63
+ uv run examples/simple.py
64
+ ```
65
+
66
+ Run tests:
67
+ ```bash
68
+ uv run pytest
69
+ ```
70
+
71
+ ## Usage
72
+
73
+ Add the following to your `__main__.py` or the equivalent module which is your entry point.
74
+
75
+ ```python
76
+ try:
77
+ import beautiful_traceback
78
+ beautiful_traceback.install()
79
+ except ImportError:
80
+ pass # no need to fail because of missing dev dependency
81
+ ```
82
+
83
+ Please do not add this code e.g. to your `__init__.py` or any other module that your users may import. They may not want you to mess with how their tracebacks are printed.
84
+
85
+ If you do feel the overwhelming desire to import the `beautiful_traceback` in code that others might import, consider using the `envvar` argument, which will cause the install function to effectively be a noop unless you set `ENABLE_BEAUTIFUL_TRACEBACK=1`.
86
+
87
+ ```python
88
+ try:
89
+ import beautiful_traceback
90
+ beautiful_traceback.install(envvar='ENABLE_BEAUTIFUL_TRACEBACK')
91
+ except ImportError:
92
+ pass # no need to fail because of missing dev dependency
93
+ ```
94
+
95
+ Note, that the hook is only installed if the existing hook is the default. Any existing hooks that were installed before the call of `beautiful_traceback.install` will be left in place.
96
+
97
+ ## LoggingFormatter
98
+
99
+ A `logging.Formatter` subclass is also available (e.g. for integration with Flask, FastAPI, etc).
100
+
101
+ ```python
102
+ import os
103
+ from flask.logging import default_handler
104
+
105
+ try:
106
+ if os.getenv('FLASK_DEBUG') == "1":
107
+ import beautiful_traceback
108
+ default_handler.setFormatter(beautiful_traceback.LoggingFormatter())
109
+ except ImportError:
110
+ pass # no need to fail because of missing dev dependency
111
+ ```
112
+
113
+ ## IPython and Jupyter Integration
114
+
115
+ Beautiful Traceback works seamlessly in IPython and Jupyter notebooks:
116
+
117
+ ```python
118
+ # Load the extension
119
+ %load_ext beautiful_traceback
120
+
121
+ # Unload if needed
122
+ %unload_ext beautiful_traceback
123
+ ```
124
+
125
+ The extension automatically installs beautiful tracebacks for your interactive session.
126
+
127
+ ## Pytest Integration
128
+
129
+ Beautiful Traceback includes a pytest plugin that automatically enhances test failure output.
130
+
131
+ ### Automatic Activation
132
+
133
+ The plugin activates automatically when `beautiful-traceback` is installed. No configuration needed!
134
+
135
+ ### Configuration Options
136
+
137
+ Customize the plugin in your `pytest.ini` or `pyproject.toml`:
138
+
139
+ ```toml
140
+ [tool.pytest.ini_options]
141
+ enable_beautiful_traceback = true # Enable/disable the plugin
142
+ enable_beautiful_traceback_local_stack_only = true # Show only local code (filter libraries)
143
+ ```
144
+
145
+ Or in `pytest.ini`:
146
+
147
+ ```ini
148
+ [pytest]
149
+ enable_beautiful_traceback = true
150
+ enable_beautiful_traceback_local_stack_only = true
151
+ ```
152
+
153
+ ## Examples
154
+
155
+ Check out the [examples/](examples/) directory for detailed usage examples including basic usage, exception chaining, logging integration, and more.
156
+
157
+ ```bash
158
+ # Quick single-exception example
159
+ uv run examples/simple.py
160
+
161
+ # Interactive demo with multiple exception types
162
+ uv run examples/demo.py
163
+ ```
164
+
165
+ ## Configuration
166
+
167
+ ### Installation Options
168
+
169
+ Beautiful Traceback supports several configuration options:
170
+
171
+ ```python
172
+ beautiful_traceback.install(
173
+ color=True, # Enable colored output
174
+ only_tty=True, # Only activate for TTY output
175
+ only_hook_if_default_excepthook=True, # Only install if default hook
176
+ local_stack_only=False, # Filter to show only local code
177
+ envvar='ENABLE_BEAUTIFUL_TRACEBACK' # Optional environment variable gate
178
+ )
179
+ ```
180
+
181
+ ### Environment Variables
182
+
183
+ - **`NO_COLOR`** - Disables colored output when set (respects [no-color.org](https://no-color.org) standard)
184
+ - **`ENABLE_BEAUTIFUL_TRACEBACK`** - Controls activation when using the `envvar` parameter (set to `1` to enable)
185
+
186
+ ### LoggingFormatterMixin
187
+
188
+ For more advanced logging integration, you can use `LoggingFormatterMixin` as a base class:
189
+
190
+ ```python
191
+ import logging
192
+ import beautiful_traceback
193
+
194
+ class MyFormatter(beautiful_traceback.LoggingFormatterMixin, logging.Formatter):
195
+ def __init__(self):
196
+ super().__init__(fmt='%(levelname)s: %(message)s')
197
+ ```
198
+
199
+ This gives you full control over the log format while adding beautiful traceback support.
200
+
201
+ ## Global Installation via PTH File
202
+
203
+ You can enable beautiful-traceback across all Python projects without modifying any source code by using a `.pth` file. Python automatically executes import statements in `.pth` files during interpreter startup, making this perfect for development environments.
204
+
205
+ Add this function to your `.zshrc` or `.bashrc`:
206
+
207
+ ```bash
208
+ # Create a file to automatically import beautiful-traceback on startup
209
+ python-inject-beautiful-traceback() {
210
+ local site_packages=$(python -c "import site; print(site.getsitepackages()[0])")
211
+
212
+ local pth_file=$site_packages/beautiful_traceback_injection.pth
213
+ local py_file=$site_packages/_beautiful_traceback_injection.py
214
+
215
+ cat <<'EOF' >"$py_file"
216
+ def run_startup_script():
217
+ try:
218
+ import beautiful_traceback
219
+ beautiful_traceback.install()
220
+ except ImportError:
221
+ pass
222
+
223
+ run_startup_script()
224
+ EOF
225
+
226
+ echo "import _beautiful_traceback_injection" >"$pth_file"
227
+ echo "Beautiful traceback injection created: $pth_file"
228
+ }
229
+ ```
230
+
231
+ After sourcing your shell config, run `python-inject-beautiful-traceback` to enable beautiful tracebacks globally for that Python environment.
232
+
233
+ ## Alternatives
234
+
235
+ Beautiful Traceback is heavily inspired by the backtrace module by [nir0s](https://github.com/nir0s/backtrace) but there are many others (sorted by github stars):
236
+
237
+ - https://github.com/qix-/better-exceptions
238
+ - https://github.com/cknd/stackprinter
239
+ - https://github.com/onelivesleft/PrettyErrors
240
+ - https://github.com/skorokithakis/tbvaccine
241
+ - https://github.com/aroberge/friendly-traceback
242
+ - https://github.com/HallerPatrick/frosch
243
+ - https://github.com/nir0s/backtrace
244
+ - https://github.com/mbarkhau/pretty-traceback
245
+ - https://github.com/staticshock/colored-traceback.py
246
+ - https://github.com/chillaranand/ptb
247
+ - https://github.com/laurb9/rich-traceback
248
+ - https://github.com/willmcgugan/rich#tracebacks
249
+
250
+ ## License
251
+
252
+ MIT License - see [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,240 @@
1
+ # Beautiful Traceback
2
+
3
+ > **Note:** This is a fork of the [pretty-traceback](https://github.com/mbarkhau/pretty-traceback) repo with simplified development and improvements for better integration with FastAPI, [structlog](https://github.com/iloveitaly/structlog-config), IPython, pytest, and more. This project is used in [python-starter-template](https://github.com/iloveitaly/python-starter-template) to provide better debugging experience in production environments.
4
+
5
+ Human readable stacktraces for Python.
6
+
7
+ [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md)
8
+ [![Python Versions](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
9
+
10
+ ## Quick Start
11
+
12
+ The fastest way to see it in action:
13
+
14
+ ```bash
15
+ # Clone and run an example
16
+ git clone https://github.com/iloveitaly/beautiful-traceback
17
+ cd beautiful-traceback
18
+ uv run examples/simple.py
19
+ ```
20
+
21
+ ## Overview
22
+
23
+ Beautiful Traceback groups together what belongs together, adds coloring and alignment. All of this makes it easier for you to see patterns and filter out the signal from the noise. This tabular format is best viewed in a wide terminal.
24
+
25
+ ![Comparison of standard Python traceback vs Beautiful Traceback](comparison.webp)
26
+
27
+ ## Installation
28
+
29
+ ### From PyPI (when published)
30
+
31
+ ```bash
32
+ # Using uv (recommended)
33
+ uv add beautiful-traceback
34
+
35
+ # Using pip
36
+ pip install beautiful-traceback
37
+ ```
38
+
39
+ ### Development Installation
40
+
41
+ To install from source:
42
+
43
+ ```bash
44
+ git clone https://github.com/iloveitaly/beautiful-traceback
45
+ cd beautiful-traceback
46
+ uv sync
47
+ ```
48
+
49
+ Run examples:
50
+ ```bash
51
+ uv run examples/simple.py
52
+ ```
53
+
54
+ Run tests:
55
+ ```bash
56
+ uv run pytest
57
+ ```
58
+
59
+ ## Usage
60
+
61
+ Add the following to your `__main__.py` or the equivalent module which is your entry point.
62
+
63
+ ```python
64
+ try:
65
+ import beautiful_traceback
66
+ beautiful_traceback.install()
67
+ except ImportError:
68
+ pass # no need to fail because of missing dev dependency
69
+ ```
70
+
71
+ Please do not add this code e.g. to your `__init__.py` or any other module that your users may import. They may not want you to mess with how their tracebacks are printed.
72
+
73
+ If you do feel the overwhelming desire to import the `beautiful_traceback` in code that others might import, consider using the `envvar` argument, which will cause the install function to effectively be a noop unless you set `ENABLE_BEAUTIFUL_TRACEBACK=1`.
74
+
75
+ ```python
76
+ try:
77
+ import beautiful_traceback
78
+ beautiful_traceback.install(envvar='ENABLE_BEAUTIFUL_TRACEBACK')
79
+ except ImportError:
80
+ pass # no need to fail because of missing dev dependency
81
+ ```
82
+
83
+ Note, that the hook is only installed if the existing hook is the default. Any existing hooks that were installed before the call of `beautiful_traceback.install` will be left in place.
84
+
85
+ ## LoggingFormatter
86
+
87
+ A `logging.Formatter` subclass is also available (e.g. for integration with Flask, FastAPI, etc).
88
+
89
+ ```python
90
+ import os
91
+ from flask.logging import default_handler
92
+
93
+ try:
94
+ if os.getenv('FLASK_DEBUG') == "1":
95
+ import beautiful_traceback
96
+ default_handler.setFormatter(beautiful_traceback.LoggingFormatter())
97
+ except ImportError:
98
+ pass # no need to fail because of missing dev dependency
99
+ ```
100
+
101
+ ## IPython and Jupyter Integration
102
+
103
+ Beautiful Traceback works seamlessly in IPython and Jupyter notebooks:
104
+
105
+ ```python
106
+ # Load the extension
107
+ %load_ext beautiful_traceback
108
+
109
+ # Unload if needed
110
+ %unload_ext beautiful_traceback
111
+ ```
112
+
113
+ The extension automatically installs beautiful tracebacks for your interactive session.
114
+
115
+ ## Pytest Integration
116
+
117
+ Beautiful Traceback includes a pytest plugin that automatically enhances test failure output.
118
+
119
+ ### Automatic Activation
120
+
121
+ The plugin activates automatically when `beautiful-traceback` is installed. No configuration needed!
122
+
123
+ ### Configuration Options
124
+
125
+ Customize the plugin in your `pytest.ini` or `pyproject.toml`:
126
+
127
+ ```toml
128
+ [tool.pytest.ini_options]
129
+ enable_beautiful_traceback = true # Enable/disable the plugin
130
+ enable_beautiful_traceback_local_stack_only = true # Show only local code (filter libraries)
131
+ ```
132
+
133
+ Or in `pytest.ini`:
134
+
135
+ ```ini
136
+ [pytest]
137
+ enable_beautiful_traceback = true
138
+ enable_beautiful_traceback_local_stack_only = true
139
+ ```
140
+
141
+ ## Examples
142
+
143
+ Check out the [examples/](examples/) directory for detailed usage examples including basic usage, exception chaining, logging integration, and more.
144
+
145
+ ```bash
146
+ # Quick single-exception example
147
+ uv run examples/simple.py
148
+
149
+ # Interactive demo with multiple exception types
150
+ uv run examples/demo.py
151
+ ```
152
+
153
+ ## Configuration
154
+
155
+ ### Installation Options
156
+
157
+ Beautiful Traceback supports several configuration options:
158
+
159
+ ```python
160
+ beautiful_traceback.install(
161
+ color=True, # Enable colored output
162
+ only_tty=True, # Only activate for TTY output
163
+ only_hook_if_default_excepthook=True, # Only install if default hook
164
+ local_stack_only=False, # Filter to show only local code
165
+ envvar='ENABLE_BEAUTIFUL_TRACEBACK' # Optional environment variable gate
166
+ )
167
+ ```
168
+
169
+ ### Environment Variables
170
+
171
+ - **`NO_COLOR`** - Disables colored output when set (respects [no-color.org](https://no-color.org) standard)
172
+ - **`ENABLE_BEAUTIFUL_TRACEBACK`** - Controls activation when using the `envvar` parameter (set to `1` to enable)
173
+
174
+ ### LoggingFormatterMixin
175
+
176
+ For more advanced logging integration, you can use `LoggingFormatterMixin` as a base class:
177
+
178
+ ```python
179
+ import logging
180
+ import beautiful_traceback
181
+
182
+ class MyFormatter(beautiful_traceback.LoggingFormatterMixin, logging.Formatter):
183
+ def __init__(self):
184
+ super().__init__(fmt='%(levelname)s: %(message)s')
185
+ ```
186
+
187
+ This gives you full control over the log format while adding beautiful traceback support.
188
+
189
+ ## Global Installation via PTH File
190
+
191
+ You can enable beautiful-traceback across all Python projects without modifying any source code by using a `.pth` file. Python automatically executes import statements in `.pth` files during interpreter startup, making this perfect for development environments.
192
+
193
+ Add this function to your `.zshrc` or `.bashrc`:
194
+
195
+ ```bash
196
+ # Create a file to automatically import beautiful-traceback on startup
197
+ python-inject-beautiful-traceback() {
198
+ local site_packages=$(python -c "import site; print(site.getsitepackages()[0])")
199
+
200
+ local pth_file=$site_packages/beautiful_traceback_injection.pth
201
+ local py_file=$site_packages/_beautiful_traceback_injection.py
202
+
203
+ cat <<'EOF' >"$py_file"
204
+ def run_startup_script():
205
+ try:
206
+ import beautiful_traceback
207
+ beautiful_traceback.install()
208
+ except ImportError:
209
+ pass
210
+
211
+ run_startup_script()
212
+ EOF
213
+
214
+ echo "import _beautiful_traceback_injection" >"$pth_file"
215
+ echo "Beautiful traceback injection created: $pth_file"
216
+ }
217
+ ```
218
+
219
+ After sourcing your shell config, run `python-inject-beautiful-traceback` to enable beautiful tracebacks globally for that Python environment.
220
+
221
+ ## Alternatives
222
+
223
+ Beautiful Traceback is heavily inspired by the backtrace module by [nir0s](https://github.com/nir0s/backtrace) but there are many others (sorted by github stars):
224
+
225
+ - https://github.com/qix-/better-exceptions
226
+ - https://github.com/cknd/stackprinter
227
+ - https://github.com/onelivesleft/PrettyErrors
228
+ - https://github.com/skorokithakis/tbvaccine
229
+ - https://github.com/aroberge/friendly-traceback
230
+ - https://github.com/HallerPatrick/frosch
231
+ - https://github.com/nir0s/backtrace
232
+ - https://github.com/mbarkhau/pretty-traceback
233
+ - https://github.com/staticshock/colored-traceback.py
234
+ - https://github.com/chillaranand/ptb
235
+ - https://github.com/laurb9/rich-traceback
236
+ - https://github.com/willmcgugan/rich#tracebacks
237
+
238
+ ## License
239
+
240
+ MIT License - see [LICENSE.md](LICENSE.md) for details.
@@ -0,0 +1,22 @@
1
+ from .hook import install
2
+ from .hook import uninstall
3
+ from .formatting import LoggingFormatter
4
+ from .formatting import LoggingFormatterMixin
5
+
6
+ from ._extension import load_ipython_extension # noqa: F401
7
+
8
+ __version__ = "0.1.0"
9
+
10
+
11
+ # retain typo for backward compatibility
12
+ LoggingFormaterMixin = LoggingFormatterMixin
13
+
14
+
15
+ __all__ = [
16
+ "install",
17
+ "uninstall",
18
+ "__version__",
19
+ "LoggingFormatter",
20
+ "LoggingFormatterMixin",
21
+ "LoggingFormaterMixin",
22
+ ]
@@ -0,0 +1,14 @@
1
+ from typing import Any
2
+
3
+
4
+ def load_ipython_extension(ip: Any) -> None: # pragma: no cover
5
+ # prevent circular import
6
+ from beautiful_traceback import install
7
+
8
+ install()
9
+
10
+
11
+ def unload_ipython_extension(ip: Any) -> None: # pragma: no cover
12
+ from beautiful_traceback import uninstall
13
+
14
+ uninstall()
@@ -0,0 +1,28 @@
1
+ import typing as typ
2
+
3
+
4
+ class Entry(typ.NamedTuple):
5
+ module: str
6
+ call: str
7
+ lineno: str
8
+ src_ctx: str
9
+
10
+
11
+ Entries = typ.List[Entry]
12
+
13
+
14
+ class Traceback(typ.NamedTuple):
15
+ exc_name: str
16
+ exc_msg: str
17
+ entries: Entries
18
+
19
+ is_caused: bool
20
+ is_context: bool
21
+
22
+
23
+ Tracebacks = typ.List[Traceback]
24
+
25
+ ALIASES_HEAD = "Aliases for entries in sys.path:"
26
+ TRACEBACK_HEAD = "Traceback (most recent call last):"
27
+ CAUSE_HEAD = "The above exception was the direct cause of the following exception:"
28
+ CONTEXT_HEAD = "During handling of the above exception, another exception occurred:"
@@ -0,0 +1,512 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import types
5
+ import typing as typ
6
+ import logging
7
+ import traceback as tb
8
+ import subprocess as sp
9
+ import collections
10
+
11
+ import colorama
12
+
13
+ import beautiful_traceback.common as com
14
+
15
+ DEFAULT_COLUMNS = 80
16
+
17
+
18
+ def _get_terminal_width() -> int:
19
+ try:
20
+ columns = int(os.environ["COLUMNS"])
21
+ # lines = int(os.environ['LINES' ])
22
+ return columns
23
+ except (KeyError, ValueError):
24
+ pass
25
+
26
+ if not sys.stdout.isatty():
27
+ return DEFAULT_COLUMNS
28
+
29
+ if hasattr(os, "get_terminal_size"):
30
+ try:
31
+ size = os.get_terminal_size(0)
32
+ return size.columns
33
+ except OSError:
34
+ pass
35
+
36
+ try:
37
+ size_output = sp.check_output(["stty", "size"]).decode()
38
+ _, columns = [int(val) for val in size_output.strip().split()]
39
+ return columns
40
+ except sp.CalledProcessError:
41
+ pass
42
+ except IOError:
43
+ pass
44
+
45
+ return DEFAULT_COLUMNS
46
+
47
+
48
+ FMT_MODULE: str = (
49
+ colorama.Fore.CYAN + colorama.Style.NORMAL + "{0}" + colorama.Style.RESET_ALL
50
+ )
51
+ FMT_CALL: str = (
52
+ colorama.Fore.YELLOW + colorama.Style.NORMAL + "{0}" + colorama.Style.RESET_ALL
53
+ )
54
+ FMT_LINENO: str = (
55
+ colorama.Fore.MAGENTA + colorama.Style.NORMAL + "{0}" + colorama.Style.RESET_ALL
56
+ )
57
+ FMT_CONTEXT: str = "{0}"
58
+
59
+ FMT_ERROR_NAME: str = (
60
+ colorama.Fore.RED + colorama.Style.BRIGHT + "{0}" + colorama.Style.RESET_ALL
61
+ )
62
+ FMT_ERROR_MSG: str = colorama.Style.BRIGHT + "{0}" + colorama.Style.RESET_ALL
63
+
64
+
65
+ class Row(typ.NamedTuple):
66
+ alias: str
67
+ short_module: str
68
+ full_module: str
69
+ call: str
70
+ lineno: str
71
+ context: str
72
+
73
+
74
+ class PaddedRow(typ.NamedTuple):
75
+ alias: str
76
+ short_module: str
77
+ full_module: str
78
+ call: str
79
+ lineno: str
80
+ context: str
81
+
82
+
83
+ Alias = str
84
+ Prefix = str
85
+
86
+ AliasPrefix = typ.Tuple[Alias, Prefix]
87
+ AliasPrefixes = typ.List[AliasPrefix]
88
+
89
+
90
+ class Context(typ.NamedTuple):
91
+ rows: typ.List[Row]
92
+ aliases: AliasPrefixes
93
+
94
+ max_row_width: int
95
+ is_wide_mode: bool
96
+
97
+ # for paddings
98
+ max_short_module_len: int
99
+ max_full_module_len: int
100
+
101
+ max_lineno_len: int
102
+ max_call_len: int
103
+ max_context_len: int
104
+
105
+
106
+ def _iter_entry_paths(entries: com.Entries) -> typ.Iterable[str]:
107
+ for entry in entries:
108
+ module_abspath = os.path.abspath(entry.module)
109
+ is_valid_abspath = module_abspath != entry.module and os.path.exists(
110
+ module_abspath
111
+ )
112
+ if is_valid_abspath:
113
+ yield module_abspath
114
+ else:
115
+ yield entry.module
116
+
117
+
118
+ # used by unit tests to override paths
119
+ TEST_PATHS: typ.List[str] = []
120
+
121
+ PWD = os.getcwd()
122
+
123
+
124
+ def _py_paths() -> typ.List[str]:
125
+ if TEST_PATHS:
126
+ return TEST_PATHS
127
+
128
+ # NOTE (mb 2020-08-16): We don't know which path entry
129
+ # was used to import a module. I guess we could figure it
130
+ # out, but the preference here is to make the shortest
131
+ # path possible.
132
+
133
+ paths = list(sys.path)
134
+ # NOTE (mb 2020-10-04): aliases must be sorted from longest to
135
+ # shortest, so that the longer matches are used first.
136
+ paths.sort(key=len, reverse=True)
137
+
138
+ if "" in paths:
139
+ paths.remove("")
140
+
141
+ return paths
142
+
143
+
144
+ def _iter_used_py_paths(entry_paths: typ.List[str]) -> typ.Iterable[str]:
145
+ _uniq_entry_paths = set(entry_paths)
146
+
147
+ for py_path in _py_paths():
148
+ is_path_used = False
149
+ for entry_path in list(_uniq_entry_paths):
150
+ if entry_path.startswith(py_path):
151
+ is_path_used = True
152
+ _uniq_entry_paths.remove(entry_path)
153
+
154
+ if is_path_used:
155
+ yield py_path
156
+
157
+
158
+ def _iter_alias_prefixes(entry_paths: typ.List[str]) -> typ.Iterable[AliasPrefix]:
159
+ alias_index = 0
160
+
161
+ for py_path in _iter_used_py_paths(entry_paths):
162
+ if py_path.endswith("site-packages"):
163
+ alias = "<site>"
164
+ elif py_path.endswith("dist-packages"):
165
+ alias = "<dist>"
166
+ elif re.search(r"lib/python\d.\d+$", py_path):
167
+ alias = "<py>"
168
+ elif re.search(r"lib/Python\d.\d+\\lib$", py_path):
169
+ alias = "<py>"
170
+ elif py_path.startswith(PWD):
171
+ alias = "<pwd>"
172
+ py_path = PWD
173
+ else:
174
+ alias = f"<p{alias_index}>"
175
+ alias_index += 1
176
+
177
+ # Always end paths with a slash. This way relative paths don't
178
+ # start with a / and tooling can open files (e.g. Ctrl+Click),
179
+ # which would otherwise be parsed as absolute paths.
180
+ if not py_path.endswith("/"):
181
+ py_path = py_path + "/"
182
+
183
+ yield (alias, py_path)
184
+
185
+
186
+ def _iter_entry_rows(
187
+ aliases: AliasPrefixes, entry_paths: typ.List[str], entries: com.Entries
188
+ ) -> typ.Iterable[Row]:
189
+ for abs_module, entry in zip(entry_paths, entries):
190
+ used_alias = ""
191
+ module_full = abs_module
192
+ module_short = abs_module
193
+
194
+ module = entry.module
195
+ if module.startswith("." + os.sep):
196
+ module = module[2:]
197
+
198
+ # NOTE (mb 2020-08-18): module may not be an absolute path,
199
+ # but it's not shortened using an alias yet either.
200
+ if abs_module.endswith(module):
201
+ for alias, alias_path in aliases:
202
+ if abs_module.startswith(alias_path):
203
+ new_module_short = abs_module[len(alias_path) :]
204
+
205
+ new_len = len(new_module_short) + len(alias)
206
+ old_len = len(module_short) + len(used_alias)
207
+ if new_len < old_len:
208
+ used_alias = alias
209
+ module_short = new_module_short
210
+
211
+ yield Row(
212
+ used_alias,
213
+ module_short,
214
+ module_full,
215
+ entry.call or "",
216
+ entry.lineno or "",
217
+ entry.src_ctx or "",
218
+ )
219
+
220
+
221
+ def _init_entries_context(
222
+ entries: com.Entries, term_width: typ.Optional[int] = None
223
+ ) -> Context:
224
+ if term_width is None:
225
+ _term_width = _get_terminal_width()
226
+ else:
227
+ _term_width = term_width
228
+
229
+ entry_paths = list(_iter_entry_paths(entries))
230
+ aliases = list(_iter_alias_prefixes(entry_paths))
231
+
232
+ # NOTE (mb 2020-10-04): When calculating widths of a column, we care more
233
+ # about alignment than staying below the max_row_width. The limits are
234
+ # only a best effort and padding will be added even if that means
235
+ # wrapping. We rely on the aliases to reduce the wrapping.
236
+
237
+ # indent (4 spaces) + 3 x sep (2 spaces each)
238
+ max_row_width = _term_width - 10
239
+
240
+ rows = list(_iter_entry_rows(aliases, entry_paths, entries))
241
+
242
+ if rows:
243
+ max_short_module_len = max(
244
+ len(row.alias) + len(row.short_module) for row in rows
245
+ )
246
+ max_full_module_len = max(len(row.full_module) for row in rows)
247
+
248
+ max_lineno_len = max(len(row.lineno) for row in rows)
249
+ max_call_len = max(len(row.call) for row in rows)
250
+ max_context_len = max(len(row.context) for row in rows)
251
+ else:
252
+ max_short_module_len = 0
253
+ max_full_module_len = 0
254
+
255
+ max_lineno_len = 0
256
+ max_call_len = 0
257
+ max_context_len = 0
258
+
259
+ max_total_len = (
260
+ max_full_module_len + max_lineno_len + max_call_len + max_context_len
261
+ )
262
+ is_wide_mode = max_total_len < max_row_width
263
+
264
+ return Context(
265
+ rows,
266
+ aliases,
267
+ max_row_width,
268
+ is_wide_mode,
269
+ max_short_module_len,
270
+ max_full_module_len,
271
+ max_lineno_len,
272
+ max_call_len,
273
+ max_context_len,
274
+ )
275
+
276
+
277
+ def _padded_rows(ctx: Context) -> typ.Iterable[PaddedRow]:
278
+ # Expand padding from left to right.
279
+ # This will mutate rows (updating strings with added padding)
280
+
281
+ for row in ctx.rows:
282
+ if ctx.is_wide_mode:
283
+ short_module = ""
284
+ full_module = row.full_module.ljust(ctx.max_full_module_len)
285
+ else:
286
+ short_module = row.short_module.ljust(
287
+ ctx.max_short_module_len - len(row.alias)
288
+ )
289
+ full_module = ""
290
+
291
+ # the max lengths are calculated upstream in `_init_entries_context`
292
+ padded_call = row.call.ljust(ctx.max_call_len)
293
+ padded_lineno = row.lineno.ljust(ctx.max_lineno_len)
294
+
295
+ yield PaddedRow(
296
+ row.alias,
297
+ short_module,
298
+ full_module,
299
+ padded_call,
300
+ padded_lineno,
301
+ row.context,
302
+ )
303
+
304
+
305
+ def _aliases_to_lines(ctx: Context, color: bool = False) -> typ.Iterable[str]:
306
+ fmt_module = FMT_MODULE if color else "{0}"
307
+ if ctx.aliases:
308
+ alias_padding = max(len(alias) for alias, _ in ctx.aliases)
309
+ for alias, path in ctx.aliases:
310
+ yield " " + alias.ljust(alias_padding) + ": " + fmt_module.format(path)
311
+
312
+
313
+ def _rows_to_lines(
314
+ rows: typ.List[PaddedRow], color: bool = False, local_stack_only: bool = False
315
+ ) -> typ.Iterable[str]:
316
+ # apply colors and additional separators/ spacing
317
+ fmt_module = FMT_MODULE if color else "{0}"
318
+ fmt_call = FMT_CALL if color else "{0}"
319
+ fmt_lineno = FMT_LINENO if color else "{0}"
320
+ fmt_context = FMT_CONTEXT if color else "{0}"
321
+
322
+ # padding has already been added to the components at this point
323
+ for alias, short_module, full_module, call, lineno, context in rows:
324
+ if short_module:
325
+ _alias = alias
326
+ module = short_module
327
+ else:
328
+ _alias = ""
329
+ module = full_module
330
+
331
+ # append line number to file so editors can jump to the line
332
+ bare_module = module.strip()
333
+ bare_lineno = lineno.strip()
334
+ module_padding = " " * (
335
+ len(module) - len(bare_module) + len(lineno) - len(bare_lineno)
336
+ )
337
+
338
+ parts = (
339
+ " ",
340
+ _alias,
341
+ " ",
342
+ fmt_module.format(bare_module),
343
+ ":",
344
+ fmt_lineno.format(bare_lineno),
345
+ module_padding,
346
+ " ",
347
+ fmt_call.format(call),
348
+ " ",
349
+ fmt_context.format(context),
350
+ )
351
+
352
+ line = "".join(parts)
353
+
354
+ if local_stack_only and not alias == "<pwd>":
355
+ continue
356
+
357
+ # bold any entries which are the current working directory
358
+ if alias == "<pwd>":
359
+ yield line.replace(colorama.Style.NORMAL, colorama.Style.BRIGHT)
360
+ else:
361
+ yield line
362
+
363
+
364
+ def _traceback_to_entries(traceback: types.TracebackType) -> typ.Iterable[com.Entry]:
365
+ summary = tb.extract_tb(traceback)
366
+ for entry in summary:
367
+ module = entry[0]
368
+ call = entry[2]
369
+ lineno = str(entry[1])
370
+ context = entry[3] or ""
371
+ yield com.Entry(module, call, lineno, context)
372
+
373
+
374
+ def _format_traceback(
375
+ ctx: Context,
376
+ traceback: com.Traceback,
377
+ color: bool = False,
378
+ local_stack_only: bool = False,
379
+ ) -> str:
380
+ padded_rows = list(_padded_rows(ctx))
381
+
382
+ lines = []
383
+ if ctx.aliases and not ctx.is_wide_mode:
384
+ lines.append(com.ALIASES_HEAD)
385
+ lines.extend(_aliases_to_lines(ctx, color))
386
+
387
+ lines.append(com.TRACEBACK_HEAD)
388
+ lines.extend(_rows_to_lines(padded_rows, color, local_stack_only))
389
+
390
+ if traceback.exc_name == "RecursionError" and len(lines) > 100:
391
+ prelude_index = 0
392
+
393
+ line_counts: typ.Dict[str, int] = collections.defaultdict(int)
394
+ for i, line in enumerate(lines):
395
+ line_counts[line] += 1
396
+ if line_counts[line] == 3:
397
+ prelude_index = i
398
+ break
399
+
400
+ if prelude_index > 0:
401
+ num_omitted = len(lines) - prelude_index - 2
402
+ lines = (
403
+ lines[:prelude_index]
404
+ + [f" ... {num_omitted} omitted lines"]
405
+ + lines[-2:]
406
+ )
407
+
408
+ fmt_error_name = FMT_ERROR_NAME if color else "{0}"
409
+ error_line = fmt_error_name.format(traceback.exc_name)
410
+ if traceback.exc_msg:
411
+ fmt_error_msg = FMT_ERROR_MSG if color else "{0}"
412
+ error_line += ": " + fmt_error_msg.format(traceback.exc_msg)
413
+
414
+ lines.append(error_line)
415
+ return os.linesep.join(lines) + os.linesep
416
+
417
+
418
+ def format_traceback(
419
+ traceback: com.Traceback, color: bool = False, local_stack_only: bool = False
420
+ ) -> str:
421
+ ctx = _init_entries_context(traceback.entries)
422
+ return _format_traceback(ctx, traceback, color, local_stack_only)
423
+
424
+
425
+ def format_tracebacks(
426
+ tracebacks: typ.List[com.Traceback],
427
+ color: bool = False,
428
+ local_stack_only: bool = False,
429
+ ) -> str:
430
+ traceback_strs: typ.List[str] = []
431
+
432
+ for tb_tup in tracebacks:
433
+ if tb_tup.is_caused:
434
+ # traceback_strs.append("vvv caused by ^^^ - ")
435
+ traceback_strs.append(com.CAUSE_HEAD + os.linesep)
436
+ elif tb_tup.is_context:
437
+ # traceback_strs.append("vvv happend after ^^^ - ")
438
+ traceback_strs.append(com.CONTEXT_HEAD + os.linesep)
439
+
440
+ traceback_str = format_traceback(tb_tup, color, local_stack_only)
441
+ traceback_strs.append(traceback_str)
442
+
443
+ return os.linesep.join(traceback_strs).strip()
444
+
445
+
446
+ def get_tb_attr(ex: BaseException) -> types.TracebackType:
447
+ return typ.cast(types.TracebackType, getattr(ex, "__traceback__", None))
448
+
449
+
450
+ def exc_to_traceback_str(
451
+ exc_value: BaseException,
452
+ traceback: types.TracebackType,
453
+ color: bool = False,
454
+ local_stack_only: bool = False,
455
+ ) -> str:
456
+ # NOTE (mb 2020-08-13): wrt. cause vs context see
457
+ # https://www.python.org/dev/peps/pep-3134/#enhanced-reporting
458
+ # https://stackoverflow.com/questions/11235932/
459
+ tracebacks: typ.List[com.Traceback] = []
460
+
461
+ cur_exc_value: BaseException = exc_value
462
+ cur_traceback: types.TracebackType = traceback
463
+
464
+ # Track seen exceptions to prevent infinite loops from circular references
465
+ seen_exceptions: typ.Set[int] = set()
466
+
467
+ while cur_exc_value:
468
+ # Check if we've seen this exception before (circular reference)
469
+ exc_id = id(cur_exc_value)
470
+ if exc_id in seen_exceptions:
471
+ # Circular reference detected, break the loop
472
+ break
473
+ seen_exceptions.add(exc_id)
474
+
475
+ next_cause = getattr(cur_exc_value, "__cause__", None)
476
+ next_context = getattr(cur_exc_value, "__context__", None)
477
+
478
+ tb_tup = com.Traceback(
479
+ exc_name=type(cur_exc_value).__name__,
480
+ exc_msg=str(cur_exc_value),
481
+ entries=list(_traceback_to_entries(cur_traceback)),
482
+ is_caused=bool(next_cause),
483
+ is_context=bool(next_context),
484
+ )
485
+
486
+ tracebacks.append(tb_tup)
487
+
488
+ if next_cause:
489
+ cur_exc_value = next_cause
490
+ cur_traceback = get_tb_attr(next_cause)
491
+ elif next_context:
492
+ cur_exc_value = next_context
493
+ cur_traceback = get_tb_attr(next_context)
494
+ else:
495
+ break
496
+
497
+ tracebacks = list(reversed(tracebacks))
498
+
499
+ return format_tracebacks(tracebacks, color, local_stack_only)
500
+
501
+
502
+ class LoggingFormatterMixin:
503
+ # pylint:disable=invalid-name # logging module naming convention
504
+ # pylint:disable=no-self-use # because mixin
505
+
506
+ def formatException(self, ei) -> str:
507
+ _, exc_value, traceback = ei
508
+ return exc_to_traceback_str(exc_value, traceback, color=True)
509
+
510
+
511
+ class LoggingFormatter(LoggingFormatterMixin, logging.Formatter):
512
+ pass
@@ -0,0 +1,76 @@
1
+ import os
2
+ import sys
3
+ import types
4
+ import typing as typ
5
+
6
+ import colorama
7
+
8
+ from beautiful_traceback import formatting
9
+
10
+
11
+ def init_excepthook(color: bool, local_stack_only: bool) -> typ.Callable:
12
+ def excepthook(
13
+ exc_type: typ.Type[BaseException],
14
+ exc_value: BaseException,
15
+ traceback: types.TracebackType,
16
+ ) -> None:
17
+ # pylint:disable=unused-argument
18
+ tb_str = (
19
+ formatting.exc_to_traceback_str(
20
+ exc_value, traceback, color, local_stack_only
21
+ )
22
+ + "\n"
23
+ )
24
+ if color:
25
+ colorama.init()
26
+ try:
27
+ sys.stderr.write(tb_str)
28
+ finally:
29
+ colorama.deinit()
30
+ else:
31
+ sys.stderr.write(tb_str)
32
+
33
+ return excepthook
34
+
35
+
36
+ def install(
37
+ envvar: typ.Optional[str] = None,
38
+ color: bool = True,
39
+ only_tty: bool = True,
40
+ only_hook_if_default_excepthook: bool = True,
41
+ local_stack_only: bool = False,
42
+ ) -> None:
43
+ """Hook the current excepthook to the beautiful_traceback.
44
+
45
+ If you set `only_tty=False`, beautiful_traceback will always
46
+ be active even when stdout is piped or redirected.
47
+
48
+ Color output respects the NO_COLOR environment variable
49
+ (https://no-color.org/). If NO_COLOR is set (regardless of
50
+ its value), color output will be disabled.
51
+ """
52
+ if envvar and os.environ.get(envvar, "0") == "0":
53
+ return
54
+
55
+ # Respect NO_COLOR environment variable
56
+ if "NO_COLOR" in os.environ:
57
+ color = False
58
+
59
+ isatty = getattr(sys.stderr, "isatty", lambda: False)
60
+ if only_tty and not isatty():
61
+ return
62
+
63
+ if not isatty():
64
+ color = False
65
+
66
+ # pylint:disable=comparison-with-callable ; intentional
67
+ is_default_exepthook = sys.excepthook == sys.__excepthook__
68
+ if only_hook_if_default_excepthook and not is_default_exepthook:
69
+ return
70
+
71
+ sys.excepthook = init_excepthook(color=color, local_stack_only=local_stack_only)
72
+
73
+
74
+ def uninstall() -> None:
75
+ """Restore the default excepthook."""
76
+ sys.excepthook = sys.__excepthook__
@@ -0,0 +1,116 @@
1
+ import re
2
+ import typing as typ
3
+
4
+ import beautiful_traceback.common as com
5
+
6
+ # TODO (mb 2020-08-12): path/module with doublequotes in them.
7
+ # Not even sure what python does with that.
8
+
9
+ # https://regex101.com/r/GpKtqR/1
10
+ LOCATION_PATTERN = r"""
11
+ \s\s
12
+ File
13
+ \s
14
+ \"(?P<module>[^\"]+)\"
15
+ ,\sline\s
16
+ (?P<lineno>\d+)
17
+ ,\sin\s
18
+ (?P<call>.*)
19
+ """
20
+ LOCATION_RE = re.compile(LOCATION_PATTERN, flags=re.VERBOSE)
21
+
22
+
23
+ def _parse_entries(entry_lines: typ.List[str]) -> typ.Iterable[com.Entry]:
24
+ i = 0
25
+ while i < len(entry_lines):
26
+ line = entry_lines[i]
27
+ i += 1
28
+ loc_match = LOCATION_RE.match(line)
29
+ if loc_match is None:
30
+ continue
31
+
32
+ if i < len(entry_lines):
33
+ maybe_src_ctx = entry_lines[i]
34
+ else:
35
+ maybe_src_ctx = ""
36
+
37
+ is_src_ctx = maybe_src_ctx.startswith(" ")
38
+ if is_src_ctx:
39
+ src_ctx = maybe_src_ctx.strip()
40
+ i += 1
41
+ else:
42
+ src_ctx = ""
43
+
44
+ module, lineno, call = loc_match.groups()
45
+
46
+ yield com.Entry(module, call, lineno, src_ctx)
47
+
48
+
49
+ TRACE_HEADERS = {com.TRACEBACK_HEAD, com.CAUSE_HEAD, com.CONTEXT_HEAD}
50
+
51
+
52
+ def _iter_tracebacks(trace: str) -> typ.Iterable[com.Traceback]:
53
+ lines = trace.strip().splitlines()
54
+
55
+ i = 0
56
+ while i < len(lines):
57
+ line = lines[i].strip()
58
+
59
+ # skip empty lines
60
+ if not line:
61
+ i += 1
62
+ continue
63
+
64
+ is_caused = False
65
+ is_context = False
66
+
67
+ if line.startswith(com.CAUSE_HEAD):
68
+ is_caused = True
69
+ i += 1
70
+ elif line.startswith(com.CONTEXT_HEAD):
71
+ is_context = True
72
+ i += 1
73
+
74
+ # skip empty lines and tb head
75
+ while i < len(lines):
76
+ line = lines[i].strip()
77
+ if not line or line.startswith(com.TRACEBACK_HEAD):
78
+ i += 1
79
+ else:
80
+ break
81
+
82
+ # accumulate entry lines
83
+ entry_lines: typ.List[str] = []
84
+ while i < len(lines) and lines[i].startswith(" "):
85
+ entry_lines.append(lines[i])
86
+ i += 1
87
+
88
+ exc_line = lines[i]
89
+ if ": " in exc_line:
90
+ exc_name, exc_msg = exc_line.split(": ", 1)
91
+ else:
92
+ exc_name = exc_line
93
+ exc_msg = ""
94
+
95
+ entries = list(_parse_entries(entry_lines))
96
+ yield com.Traceback(
97
+ exc_name=exc_name,
98
+ exc_msg=exc_msg,
99
+ entries=entries,
100
+ is_caused=is_caused,
101
+ is_context=is_context,
102
+ )
103
+
104
+ i += 1
105
+
106
+
107
+ def parse_tracebacks(trace: str) -> com.Tracebacks:
108
+ """Parses a chain of tracebacks.
109
+
110
+ Args:
111
+ trace: The traceback in the default python format, starting with
112
+ "Traceback (most recent call last):"
113
+ ending with the last line in the chain, e.g.
114
+ "FileNotFoundError: [Errno 2] No such ..."
115
+ """
116
+ return list(_iter_tracebacks(trace))
@@ -0,0 +1,83 @@
1
+ from . import formatting
2
+ import pytest
3
+
4
+ from pytest import Config
5
+
6
+
7
+ def _get_option(config: Config, key: str):
8
+ val = None
9
+
10
+ # will throw an exception if option is not set
11
+ try:
12
+ val = config.getoption(key)
13
+ except Exception:
14
+ pass
15
+
16
+ if val is None:
17
+ val = config.getini(key)
18
+
19
+ return val
20
+
21
+
22
+ def pytest_addoption(parser):
23
+ parser.addini(
24
+ "enable_beautiful_traceback",
25
+ "Enable the beautiful traceback plugin",
26
+ type="bool",
27
+ default=True,
28
+ )
29
+
30
+ parser.addini(
31
+ "enable_beautiful_traceback_local_stack_only",
32
+ "Show only local code (filter out library/framework internals)",
33
+ type="bool",
34
+ default=True,
35
+ )
36
+
37
+
38
+ @pytest.hookimpl(hookwrapper=True)
39
+ def pytest_runtest_makereport(item, call):
40
+ """
41
+ Pytest stack traces are challenging to work with by default. This plugin allows beautiful_traceback to be used instead.
42
+
43
+ This little piece of code was hard-won:
44
+
45
+ https://grok.com/share/bGVnYWN5_951be3b1-6811-4fda-b220-c1dd72dedc31
46
+ """
47
+ outcome = yield
48
+ report = outcome.get_result() # Get the generated TestReport object
49
+
50
+ # Check if the report is for the 'call' phase (test execution) and if it failed
51
+ if _get_option(item.config, "enable_beautiful_traceback") and report.failed:
52
+ value = call.excinfo.value
53
+ tb = call.excinfo.tb
54
+
55
+ formatted_traceback = formatting.exc_to_traceback_str(
56
+ value,
57
+ tb,
58
+ color=True,
59
+ local_stack_only=_get_option(
60
+ item.config, "enable_beautiful_traceback_local_stack_only"
61
+ ),
62
+ )
63
+ report.longrepr = formatted_traceback
64
+
65
+
66
+ def pytest_exception_interact(node, call, report):
67
+ """
68
+ This can run during collection, not just test execution.
69
+
70
+ So, if there's an import or other pre-run error in pytest, this will apply the correct formatting.
71
+ """
72
+ if report.failed:
73
+ value = call.excinfo.value
74
+ tb = call.excinfo.tb
75
+ formatted_traceback = formatting.exc_to_traceback_str(
76
+ value,
77
+ tb,
78
+ color=True,
79
+ local_stack_only=_get_option(
80
+ node.config, "enable_beautiful_traceback_local_stack_only"
81
+ ),
82
+ )
83
+ report.longrepr = formatted_traceback
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "beautiful-traceback"
3
+ version = "0.1.0"
4
+ description = "Beautiful, readable Python tracebacks with colors and formatting"
5
+ keywords = ["traceback", "error", "debugging", "formatting"]
6
+ readme = "README.md"
7
+ requires-python = ">=3.9"
8
+ dependencies = ["colorama>=0.4.6"]
9
+ authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
10
+ urls = { "Repository" = "https://github.com/iloveitaly/beautiful-traceback" }
11
+
12
+ [build-system]
13
+ requires = ["uv_build>=0.8.11,<0.9.0"]
14
+ build-backend = "uv_build"
15
+
16
+ [tool.uv.build-backend]
17
+ # avoids the src/ directory structure
18
+ module-root = ""
19
+
20
+ [dependency-groups]
21
+ dev = ["pytest>=8.3.3"]
22
+
23
+ [project.entry-points.pytest11]
24
+ beautiful_traceback = "beautiful_traceback.pytest_plugin"
25
+
26
+