├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CHANGELIST.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.bat ├── example.png ├── example2.png ├── exampleMono.png ├── multiexcept.py ├── pretty_errors ├── __init__.py └── __main__.py ├── setup.py ├── test.py └── upload.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: onelivesleft 4 | custom: ['https://twitch.tv/onelivesleft'] 5 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 4 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ropeproject 2 | __pycache__ 3 | build 4 | pretty_errors.egg-info 5 | dist 6 | .vscode 7 | *.pyc 8 | -------------------------------------------------------------------------------- /CHANGELIST.md: -------------------------------------------------------------------------------- 1 | # v1.2.25 2 | 3 | * Fixed problem installing into virtualenv paths. 4 | 5 | 6 | # v1.2.24 7 | 8 | * Fixed problem when running in container which reports a terminal width of 0 9 | 10 | 11 | # v1.2.23 12 | 13 | * Fixed problem installing into virtualenv paths. 14 | 15 | 16 | # v1.2.22 17 | 18 | * Fixed problem installing into virtualenv paths. 19 | 20 | 21 | # v1.2.21 22 | 23 | * Fixed problem installing into paths with `\` in them. 24 | 25 | 26 | # v1.2.20 27 | 28 | * Added `ExceptionWriter.write_link` 29 | 30 | 31 | # v1.2.19 32 | 33 | * Fixed line number discrepancies in example image, added example image for `mono()` mode. 34 | * Autogenerated `configure()` call now has each entry commented out. 35 | * Added environment variable `PYTHON_PRETTY_ERRORS_ISATTY_ONLY`. If set to non-zero then `pretty_errors` will check if it is outputing to an interactive terminal, and only activate if it is (so logging to file will retain the standard Python exception formatting). 36 | * Added `__version__` 37 | 38 | 39 | # v1.2.18 40 | 41 | * Now outputs `filename` and `filename2` attributes of exceptions (e.g. in `FileNotFoundError`) 42 | * Added `exception_file_color` config option to color them. 43 | 44 | 45 | # v1.2.17 46 | 47 | * Suppressed exceptions no longer displayed (i.e. respects `suppress_context` flag) 48 | * Added `show_suppressed` config option to override this behaviour. 49 | 50 | 51 | # v1.2.16 52 | 53 | * Added support for when exceptions are thrown within exceptions. 54 | * Added `inner_exception_message` and `inner_exception_separator` config options. 55 | 56 | 57 | # v1.2.15 58 | 59 | * Really fix bug when used in interactive interpreter. 60 | 61 | 62 | # v1.2.14 63 | 64 | * Fix bug when used in interactive interpreter. 65 | 66 | 67 | # v1.2.13 68 | 69 | * No, really, fix the README URLs 70 | 71 | 72 | # v1.2.12 73 | 74 | * Fix README image URLs 75 | 76 | 77 | # v1.2.11 78 | 79 | * Fix bug when reporting SyntaxError in interactive interpreter. 80 | 81 | 82 | # v1.2.10 83 | 84 | * Minor fixes 85 | 86 | 87 | # v1.2.9 88 | 89 | * Added `replace_stderr` function to allow for situation where `pretty_errors` cannot gain access to `sys.excepthook`. 90 | * Moved installer script entirely into `__main__`. 91 | * Fixed not working with `venv`. 92 | * Improved automatic cleaning of startup files. 93 | 94 | 95 | # v1.2.8 96 | 97 | * Added check for startup files left over after uninstall. 98 | * Added `clean` option when running module directly. 99 | 100 | 101 | # v1.2.7 102 | 103 | * Fixed `point_at` bug. 104 | * Added color constants: `BLACK`, `RED`, `GREEN`, `YELLOW`, `BLUE`, `MAGENTA`, `CYAN`, `WHITE`, `BRIGHT_BLACK`, `BRIGHT_RED`, `BRIGHT_GREEN`, `BRIGHT_YELLOW`, `BRIGHT_BLUE`, `BRIGHT_MAGENTA`, `BRIGHT_CYAN`, `BRIGHT_WHITE`, `BLACK_BACKGROUND`, `RED_BACKGROUND`, `GREEN_BACKGROUND`, `YELLOW_BACKGROUND`, `BLUE_BACKGROUND`, `MAGENTA_BACKGROUND`, `CYAN_BACKGROUND`, `WHITE_BACKGROUND` 105 | 106 | 107 | # v1.2.6 108 | 109 | * Fixed `pathed_config` bug. 110 | * Added `activate` call. 111 | * Added arrow pointing to syntax error. 112 | * Added config options `display_arrow`, `arrow_head_character`, `arrow_head_color`, `arrow_tail_character`, `arrow_tail_color`, `syntax_error_color` 113 | 114 | 115 | # v1.2.5 116 | 117 | * Fix comment skeleton in *customize.py output. 118 | 119 | 120 | # v1.2.4 121 | 122 | * README updated. 123 | 124 | 125 | # v1.2.3 126 | 127 | * Added `ExceptionWriter` class to allow for overriding. 128 | * Added `pathed_config` call to allow for multiple configs, activated by path of code file in frame. 129 | * Added environment variable `PYTHON_PRETTY_ERRORS`. If set to `0` then `pretty_errors` will be disabled. 130 | 131 | 132 | # v1.2.2 133 | 134 | * `separator_character` can now be set to `None` or `''` to disable header. 135 | * Improved install wizard. 136 | * Spelling corrections. 137 | 138 | 139 | # v1.2.1 140 | 141 | * Fix for Python 2 142 | 143 | 144 | # v1.2.0 145 | 146 | * Added `mono` function to set useful config options for a monochrome terminal. 147 | * Added config option `timestamp_function` 148 | * Added `default_config` for reference. 149 | * May now use any characters in color prefixes (not just escape sequences) 150 | * Removed config options `line_prefix`, `code_prefix`, `line_prefix_color`, `code_prefix_color` - no longer needed because of above. 151 | 152 | 153 | # v 1.1.11 154 | 155 | * Fix for python 2 156 | 157 | 158 | # v1.1.10 159 | 160 | * Added `__main__` for running with `-m` 161 | * Removed post-install code (it doesn't work with `pip` - `pip` has no post-install hook) 162 | 163 | 164 | # v1.1.9 165 | 166 | * Automated adding to `sitecustomize.py` 167 | 168 | 169 | # v1.1.8 170 | 171 | * Added config options `always_display_bottom`, `truncate_locals`, `truncate_code`, `line_prefix`, `code_prefix`, `line_prefix_color`, `code_prefix_color`, `local_len_color` 172 | 173 | 174 | # v1.1.7 175 | 176 | * Site customize instructions. 177 | 178 | 179 | # v1.1.6 180 | 181 | * Added config options `display_locals`, `display_trace_locals`, `local_name_color`, `local_value_color` 182 | 183 | 184 | # v1.1.5 185 | 186 | * Nada 187 | 188 | 189 | # v1.1.4 190 | 191 | * `colorama` dependency 192 | 193 | 194 | # v1.1.3 195 | 196 | * Added `whitelist` and `blacklist` 197 | 198 | 199 | # v1.1.2 200 | 201 | * Fix README 202 | 203 | 204 | # v1.1.1 205 | 206 | * Python 2.7 compatibility 207 | 208 | 209 | # v1.1.0 210 | 211 | * Reworked to replace `sys.excepthook` instead of `sys.stderr` 212 | * Now automatically works out line length if line length is `0` (which is the default) 213 | * Added config options: `top_first`, `stack_depth`, `exception_above`, `exception_below`, `lines_before`, `lines_after`, `trace_lines_before`, `trace_lines_after`, `line_color`, `code_color` , `exception_color`, `exception_arg_color`, `prefix`, `infix`, `postfix`, `line_number_first` 214 | * Removed config option: `default_color` 215 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Pretty Errors in now in a fairly stable place, and I don't see it expanding in scope or gaining many new features. I don't recommend submitting PRs, as they are unlikely to be merged. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 onelivesleft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pretty-errors 2 | 3 | Prettifies Python exception output to make it legible. Install it with 4 | ```bash 5 | python -m pip install pretty_errors 6 | ``` 7 | 8 | If you want `pretty_errors` to be used whenever you run a python script you must add it to your python startup procedure. You can do so easily by running: 9 | ```bash 10 | python -m pretty_errors 11 | ``` 12 | This is the recommended way to use `pretty_errors`; apart from being simpler and universal, using it will mean `SyntaxError` exceptions also get formatted prettily (which doesn't work if you are manually importing `pretty_errors`). 13 | 14 | --- 15 | 16 | ![Example](https://raw.githubusercontent.com/onelivesleft/PrettyErrors/master/example.png) 17 | 18 | --- 19 | 20 | If you have not installed it universally you can use it in your project simply by importing it: 21 | ```python 22 | import pretty_errors 23 | ``` 24 | Note you need to be running in a terminal capable of colour output in order to get colour output: in Windows this means powershell, cmder, etc. If you must use a monochrome terminal then you can call the helper function `pretty_errors.mono()`, which will set some config options in a way that is useful for monochrome output. 25 | 26 | ![Monochrome](https://raw.githubusercontent.com/onelivesleft/PrettyErrors/master/exampleMono.png) 27 | 28 | If you want to configure the output then use `pretty_errors.configure()`, `pretty_errors.whitelist()`, `pretty_errors.blacklist()`, `pretty_errors.pathed_config()`. For example: 29 | ```python 30 | import pretty_errors 31 | pretty_errors.configure( 32 | separator_character = '*', 33 | filename_display = pretty_errors.FILENAME_EXTENDED, 34 | line_number_first = True, 35 | display_link = True, 36 | lines_before = 5, 37 | lines_after = 2, 38 | line_color = pretty_errors.RED + '> ' + pretty_errors.default_config.line_color, 39 | code_color = ' ' + pretty_errors.default_config.line_color, 40 | truncate_code = True, 41 | display_locals = True 42 | ) 43 | pretty_errors.blacklist('c:/python') 44 | ``` 45 | 46 | ![Result](https://raw.githubusercontent.com/onelivesleft/PrettyErrors/master/example2.png) 47 | 48 | --- 49 | 50 | ##### Scraping STDERR 51 | 52 | Sometimes it will be impossible for `pretty_errors` to utilize `sys.excepthook`: for instance, if you are using a framework which installs its own logging (such as `uvicorn`). In such cases, you can make `pretty_errors` scrape the output to `stderr` instead, replacing it with its own. To do so simple call: 53 | ```python 54 | pretty_errors.replace_stderr() 55 | ``` 56 | Note that this will lose some functionality, since `pretty_errors` will only have access to what is being output on screen, rather then the entire stack trace. A good API will generally have a way to interact with the exception stack, which will allow for using `excepthook`: `replace_stderr` should be the last resort. [See this comment for an example](https://github.com/onelivesleft/PrettyErrors/issues/16#issuecomment-751463605) 57 | 58 | --- 59 | 60 | ##### Whitelist / Blacklist: 61 | 62 | You may use the functions `whitelist(path)` and `blacklist(path)` to add paths which will be necessary (`whitelist`) or excluded (`blacklist`). The top frame of the stack is never excluded. 63 | 64 | --- 65 | 66 | ##### Pathed Configurations 67 | 68 | You may set up alternate configurations, which are triggered by the path to the code file of the frame. For example, if you were not interested in the system frames (those under 'c:/python') but did not want to hide them completely by using the `blacklist` you could do this: 69 | 70 | ```python 71 | meh = pretty_errors.config.copy() 72 | meh.line_color = meh.code_color = meh.filename_color = meh.function_color = meh.line_number_color = ( 73 | pretty_errors.GREY 74 | ) 75 | pretty_errors.pathed_config(meh, 'c:/python') 76 | ``` 77 | 78 | --- 79 | 80 | ##### Environment Variables 81 | 82 | * PYTHON_PRETTY_ERRORS
83 | You may disable `pretty_errors` by setting the environment variable `PYTHON_PRETTY_ERRORS` to `0`. i.e. at a command prompt: 84 | ```bash 85 | set PYTHON_PRETTY_ERRORS=0 86 | ``` 87 | 88 | Calling `pretty_errors.activate()` will override this. 89 | 90 | If you wish to selectively utilize `pretty_errors`, then use the above, and in your code perform your calculation to determine whether or not to call `pretty_errors.activate()`. 91 | 92 | * PYTHON_PRETTY_ERRORS_ISATTY_ONLY
93 | It may be desirable to disable `pretty_errors` when you are redirecting output to a file (to keep error logs, for instance). If you wish to do so, then setting `PYTHON_PRETTY_ERRORS_ISATTY_ONLY` to non-zero will cause `pretty_errors` to check if it is running in an interactive terminal, and only activate if so. 94 | 95 | ```bash 96 | set PYTHON_PRETTY_ERRORS_ISATTY_ONLY=1 97 | ``` 98 | 99 | Setting this will disable `replace_stderr()` in the same situations, unless you call it with the `force` parameter: `replace_stderr(force=True)`. 100 | 101 | Calling `pretty_errors.activate()` will override this. 102 | 103 | You may check `pretty_errors.terminal_is_interactive` to see if the terminal is interactive (`pretty_errors` sets this by checking `sys.stderr.isatty()`). You can use this to select a different config. For example: 104 | 105 | ```python 106 | if not pretty_errors.terminal_is_interactive: 107 | pretty_errors.mono() 108 | ``` 109 | 110 | 111 | --- 112 | 113 | ##### Configuration settings: 114 | 115 | Configuration settings are stored in `pretty_errors.config`, though should be set using `pretty_errors.configure()`. A reference for the default config is stored in `pretty_errors.default_config`. 116 | 117 | * `name`
118 | Optional field to store config name in. 119 | 120 | * `line_length`
121 | Output will be wrapped at this point. If set to `0` (which is the default) it will automatically match your console width. 122 | 123 | * `full_line_newline`
124 | Insert a hard newline even if the line is full. If `line_length` is the same as your console width and this is enabled then you will see double newlines when the line is exactly full, so usually you would only set this if they are different. 125 | 126 | * `separator_character`
127 | Character used to create the header line. Hyphen is used by default. If set to `None` or `''` then header will be disabled. 128 | 129 | * `display_timestamp`
130 | When enabled a timestamp is written in the traceback header. 131 | 132 | * `timestamp_function`
133 | Function called to generate timestamp. Default is `time.perf_counter`. 134 | 135 | * `exception_above`
136 | When enabled the exception is displayed above the stack trace. 137 | 138 | * `exception_below`
139 | When enabled the exception is displayed below the stack trace. 140 | 141 | * `stack_depth`
142 | The maximum number of entries from the stack trace to display. When `0` will display the entire stack, which is the default. 143 | 144 | * `top_first`
145 | When enabled the stack trace will be reversed, displaying the top of the stack first. 146 | 147 | * `always_display_bottom`
148 | When enabled (which is the default) the bottom frame of the stack trace will always be displayed. 149 | 150 | * `show_suppressed`
151 | When enabled all suppressed exceptions in the stack trace will be shown (typically they are suppressed because an exception above them has replaced them). The normal python behaviour is to hide them. 152 | 153 | * `filename_display`
154 | How the filename is displayed: may be `pretty_errors.FILENAME_COMPACT`, `pretty_errors.FILENAME_EXTENDED`, or `pretty_errors.FILENAME_FULL` 155 | 156 | * `line_number_first`
157 | When enabled the line number will be displayed first, rather than the filename. 158 | 159 | * `display_link`
160 | When enabled a link is written below the error location, which VSCode will allow you to click on. 161 | 162 | * `lines_after`, `lines_before`
163 | How many lines of code to display for the top frame, before and after the line the exception occurred on. 164 | 165 | * `trace_lines_after`, `trace_lines_before`
166 | How many lines of code to display for each other frame in the stack trace, before and after the line the exception occurred on. 167 | 168 | * `truncate_code`
169 | When enabled each line of code will be truncated to fit the line length. 170 | 171 | * `display_locals`
172 | When enabled, local variables appearing in the top stack frame code will be displayed with their values. 173 | 174 | * `display_trace_locals`
175 | When enabled, local variables appearing in other stack frame code will be displayed with their values. 176 | 177 | * `truncate_locals`
178 | When enabled the values of displayed local variables will be truncated to fit the line length. 179 | 180 | * `display_arrow`
181 | When enabled an arrow will be displayed for syntax errors, pointing at the offending token. 182 | 183 | * `arrow_head_character`, `arrow_tail_character`
184 | Characters used to draw the arrow which points at syntax errors. 185 | 186 | * `inner_exception_message`
187 | Message displayed when one exception occurs inside another, between the two exceptions. Default is `None`, which will simply display the exceptions separated by the header. If you want to emulate the default non-pretty behaviour, use this: 188 | 189 | `inner_exception_message = pretty_errors.MAGENTA + "\n During handling of the above exception, another exception occurred:\n"` 190 | 191 | Note that if you use `top_first` then the order will be reversed, so you should use a message like this instead: 192 | 193 | `inner_exception_message = pretty_errors.MAGENTA + "\n The above exception occurred during another exception:\n"` 194 | 195 | * `inner_exception_separator`
196 | Default is `False`. When set to `True` a header will be written before the `inner_exception_message`. 197 | 198 | * `prefix`
199 | Text string which is displayed at the top of the report, just below the header. 200 | 201 | * `infix`
202 | Text string which is displayed between each frame of the stack. 203 | 204 | * `postfix`
205 | Text string which is displayed at the bottom of the exception report. 206 | 207 | * `reset_stdout`
208 | When enabled the reset escape sequence will be written to stdout as well as stderr; turn this on if your console is being left with the wrong color. 209 | 210 | --- 211 | 212 | These color strings will be output before the relevant part of the exception message. You may include non-escape sequence strings if you wish; if you do not have a terminal which supports color output, or simply want to include extra demarcation. 213 | 214 | * `header_color`
215 | Escape sequence to set header color. 216 | 217 | * `timestamp_color`
218 | Escape sequence to set timestamp color. 219 | 220 | * `exception_color`
221 | Escape sequence to set exception color. 222 | 223 | * `exception_arg_color`
224 | Escape sequence to set exception arguments color. 225 | 226 | * `exception_file_color`
227 | Escape sequence to set color of filenames in exceptions (for example, in a FileNotFoundError). 228 | 229 | * `filename_color`
230 | Escape sequence to set filename color. 231 | 232 | * `line_number_color`
233 | Escape sequence to set line number color. 234 | 235 | * `function_color`
236 | Escape sequence to set function color. 237 | 238 | * `link_color`
239 | Escape sequence to set link color. 240 | 241 | * `line_color`
242 | Escape sequence to set the color of the line of code which caused the exception. 243 | 244 | * `code_color`
245 | Escape sequence to set the color of other displayed lines of code. 246 | 247 | * `arrow_head_color`, `arrow_tail_color`
248 | Escape sequence to set the color of the arrow which points at syntax errors. 249 | 250 | * `syntax_error_color`
251 | Escape sequence to set the color of the syntax error token. 252 | 253 | * `local_name_color`
254 | Escape sequence to set the color of local variable names. 255 | 256 | * `local_value_color`
257 | Escape sequence to set the color of local variable values. 258 | 259 | * `local_len_color`
260 | Escape sequence to set the color of local value length when local is truncated. 261 | 262 | `pretty_errors` has some built in escape sequence constants you can use when setting these colors: 263 | 264 | * `BLACK` 265 | * `GREY` 266 | * `RED` 267 | * `GREEN` 268 | * `YELLOW` 269 | * `BLUE` 270 | * `MAGENTA` 271 | * `CYAN` 272 | * `WHITE` 273 | 274 | For each color there is a matching `BRIGHT_` variant (i.e. `pretty_errors.BRIGHT_RED`), as well as a `_BACKGROUND` variant to set the background color (i.e. `pretty_errors.RED_BACKGROUND`). 275 | 276 | For example: 277 | ```python 278 | pretty_errors.configure( 279 | line_color = pretty_errors.CYAN_BACKGROUND + pretty_errors.BRIGHT_WHITE 280 | ) 281 | ``` 282 | 283 | --- 284 | 285 | ##### Further customization 286 | 287 | For the most extensive customization (short of forking the package) you may override the default `ExceptionWriter` class, allowing you to tailor the output however you wish. Typically you will only need to override the `write_` methods. 288 | 289 | For example: 290 | 291 | ```python 292 | class MyExceptionWriter(pretty_errors.ExceptionWriter): 293 | def write_header(self): 294 | self.output_text('######## ERROR ########') 295 | 296 | pretty_errors.exception_writer = MyExceptionWriter() 297 | ``` 298 | 299 | Run `help(pretty_errors.ExceptionWriter)` in the python interpreter for more details. 300 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | del dist\*.* /q 2 | \python3\python setup.py sdist bdist_wheel 3 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onelivesleft/PrettyErrors/f61574e7fec2b7118fee5a59de61a8a734459ca1/example.png -------------------------------------------------------------------------------- /example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onelivesleft/PrettyErrors/f61574e7fec2b7118fee5a59de61a8a734459ca1/example2.png -------------------------------------------------------------------------------- /exampleMono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onelivesleft/PrettyErrors/f61574e7fec2b7118fee5a59de61a8a734459ca1/exampleMono.png -------------------------------------------------------------------------------- /multiexcept.py: -------------------------------------------------------------------------------- 1 | import pretty_errors 2 | 3 | 4 | try: 5 | myval = [1,2] 6 | print(myval[3]) 7 | except: 8 | try: 9 | print(foo) 10 | except: 11 | print("First exception") 12 | raise OSError 13 | -------------------------------------------------------------------------------- /pretty_errors/__init__.py: -------------------------------------------------------------------------------- 1 | import sys, re, colorama, os, time, linecache 2 | colorama.init() 3 | output_stderr = sys.stderr 4 | terminal_is_interactive = sys.stderr.isatty() 5 | 6 | name = "pretty_errors" 7 | __version__ = "1.2.25" # remember to update version in setup.py! 8 | 9 | active = 'PYTHON_PRETTY_ERRORS' not in os.environ or os.environ['PYTHON_PRETTY_ERRORS'] != '0' 10 | interactive_tty_only = 'PYTHON_PRETTY_ERRORS_ISATTY_ONLY' in os.environ and os.environ['PYTHON_PRETTY_ERRORS_ISATTY_ONLY'] != '0' 11 | 12 | 13 | FILENAME_COMPACT = 0 14 | FILENAME_EXTENDED = 1 15 | FILENAME_FULL = 2 16 | 17 | RESET_COLOR = '\033[m' 18 | 19 | BLACK = '\033[0;30m' 20 | RED = '\033[0;31m' 21 | GREEN = '\033[0;32m' 22 | YELLOW = '\033[0;33m' 23 | BLUE = '\033[0;34m' 24 | MAGENTA = '\033[0;35m' 25 | CYAN = '\033[0;36m' 26 | WHITE = '\033[0;37m' 27 | 28 | BRIGHT_BLACK = GREY = '\033[1;30m' 29 | BRIGHT_RED = '\033[1;31m' 30 | BRIGHT_GREEN = '\033[1;32m' 31 | BRIGHT_YELLOW = '\033[1;33m' 32 | BRIGHT_BLUE = '\033[1;34m' 33 | BRIGHT_MAGENTA = '\033[1;35m' 34 | BRIGHT_CYAN = '\033[1;36m' 35 | BRIGHT_WHITE = '\033[1;37m' 36 | 37 | BLACK_BACKGROUND = '\033[40m' 38 | RED_BACKGROUND = '\033[41m' 39 | GREEN_BACKGROUND = '\033[42m' 40 | YELLOW_BACKGROUND = '\033[43m' 41 | BLUE_BACKGROUND = '\033[44m' 42 | MAGENTA_BACKGROUND = '\033[45m' 43 | CYAN_BACKGROUND = '\033[46m' 44 | WHITE_BACKGROUND = '\033[47m' 45 | 46 | 47 | ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') 48 | whitelist_paths = [] 49 | blacklist_paths = [] 50 | config_paths = {} 51 | 52 | 53 | class PrettyErrorsConfig(): 54 | def __init__(self, instance = None): 55 | if instance is None: 56 | self.name = "default" 57 | self.line_length = 0 58 | self.full_line_newline = False 59 | self.filename_display = FILENAME_COMPACT 60 | self.display_timestamp = False 61 | try: 62 | self.timestamp_function = time.perf_counter 63 | except AttributeError: 64 | self.timestamp_function = time.time 65 | self.display_link = False 66 | self.separator_character = '-' 67 | self.line_number_first = False 68 | self.top_first = False 69 | self.always_display_bottom = True 70 | self.stack_depth = 0 71 | self.exception_above = False 72 | self.exception_below = True 73 | self.trace_lines_before = 0 74 | self.trace_lines_after = 0 75 | self.lines_before = 0 76 | self.lines_after = 0 77 | self.display_locals = False 78 | self.display_trace_locals = False 79 | self.truncate_locals = True 80 | self.truncate_code = False 81 | self.display_arrow = True 82 | self.arrow_tail_character = '-' 83 | self.arrow_head_character = '^' 84 | self.header_color = GREY 85 | self.timestamp_color = GREY 86 | self.line_color = BRIGHT_WHITE 87 | self.code_color = GREY 88 | self.filename_color = BRIGHT_CYAN 89 | self.line_number_color = BRIGHT_GREEN 90 | self.function_color = BRIGHT_BLUE 91 | self.link_color = GREY 92 | self.local_name_color = BRIGHT_MAGENTA 93 | self.local_value_color = RESET_COLOR 94 | self.local_len_color = GREY 95 | self.exception_color = BRIGHT_RED 96 | self.exception_arg_color = BRIGHT_YELLOW 97 | self.exception_file_color = BRIGHT_MAGENTA 98 | self.syntax_error_color = BRIGHT_GREEN 99 | self.arrow_tail_color = BRIGHT_GREEN 100 | self.arrow_head_color = BRIGHT_GREEN 101 | self.inner_exception_message = None 102 | self.inner_exception_separator = False 103 | self.prefix = None 104 | self.infix = None 105 | self.postfix = None 106 | self.reset_stdout = False 107 | self.show_suppressed = False 108 | else: 109 | self.name = instance.name 110 | self.line_length = instance.line_length 111 | self.full_line_newline = instance.full_line_newline 112 | self.filename_display = instance.filename_display 113 | self.display_timestamp = instance.display_timestamp 114 | self.timestamp_function = instance.timestamp_function 115 | self.display_link = instance.display_link 116 | self.separator_character = instance.separator_character 117 | self.line_number_first = instance.line_number_first 118 | self.top_first = instance.top_first 119 | self.always_display_bottom = instance.always_display_bottom 120 | self.stack_depth = instance.stack_depth 121 | self.exception_above = instance.exception_above 122 | self.exception_below = instance.exception_below 123 | self.trace_lines_before = instance.trace_lines_before 124 | self.trace_lines_after = instance.trace_lines_after 125 | self.lines_before = instance.lines_before 126 | self.lines_after = instance.lines_after 127 | self.display_locals = instance.display_locals 128 | self.display_trace_locals = instance.display_trace_locals 129 | self.truncate_locals = instance.truncate_locals 130 | self.truncate_code = instance.truncate_code 131 | self.display_arrow = instance.display_arrow 132 | self.arrow_tail_character = instance.arrow_tail_character 133 | self.arrow_head_character = instance.arrow_head_character 134 | self.header_color = instance.header_color 135 | self.timestamp_color = instance.timestamp_color 136 | self.line_color = instance.line_color 137 | self.code_color = instance.code_color 138 | self.filename_color = instance.filename_color 139 | self.line_number_color = instance.line_number_color 140 | self.function_color = instance.function_color 141 | self.link_color = instance.link_color 142 | self.local_name_color = instance.local_name_color 143 | self.local_value_color = instance.local_value_color 144 | self.local_len_color = instance.local_len_color 145 | self.exception_color = instance.exception_color 146 | self.exception_arg_color = instance.exception_arg_color 147 | self.exception_file_color = instance.exception_file_color 148 | self.syntax_error_color = instance.syntax_error_color 149 | self.arrow_tail_color = instance.arrow_tail_color 150 | self.arrow_head_color = instance.arrow_head_color 151 | self.inner_exception_message = instance.inner_exception_message 152 | self.inner_exception_separator = instance.inner_exception_separator 153 | self.prefix = instance.prefix 154 | self.infix = instance.infix 155 | self.postfix = instance.postfix 156 | self.reset_stdout = instance.reset_stdout 157 | self.show_suppressed = instance.show_suppressed 158 | 159 | 160 | def configure(self, **kwargs): 161 | """Configure settings governing how exceptions are displayed.""" 162 | for setting in kwargs: 163 | if kwargs[setting] is not None: setattr(self, setting, kwargs[setting]) 164 | 165 | 166 | def copy(self): 167 | c = PrettyErrorsConfig() 168 | c.name = self.name 169 | c.line_length = self.line_length 170 | c.full_line_newline = self.full_line_newline 171 | c.filename_display = self.filename_display 172 | c.display_timestamp = self.display_timestamp 173 | c.timestamp_function = self.timestamp_function 174 | c.display_link = self.display_link 175 | c.separator_character = self.separator_character 176 | c.line_number_first = self.line_number_first 177 | c.top_first = self.top_first 178 | c.always_display_bottom = self.always_display_bottom 179 | c.stack_depth = self.stack_depth 180 | c.exception_above = self.exception_above 181 | c.exception_below = self.exception_below 182 | c.trace_lines_before = self.trace_lines_before 183 | c.trace_lines_after = self.trace_lines_after 184 | c.lines_before = self.lines_before 185 | c.lines_after = self.lines_after 186 | c.display_locals = self.display_locals 187 | c.display_trace_locals = self.display_trace_locals 188 | c.truncate_locals = self.truncate_locals 189 | c.truncate_code = self.truncate_code 190 | c.display_arrow = self.display_arrow 191 | c.arrow_tail_character = self.arrow_tail_character 192 | c.arrow_head_character = self.arrow_head_character 193 | c.header_color = self.header_color 194 | c.timestamp_color = self.timestamp_color 195 | c.line_color = self.line_color 196 | c.code_color = self.code_color 197 | c.filename_color = self.filename_color 198 | c.line_number_color = self.line_number_color 199 | c.function_color = self.function_color 200 | c.link_color = self.link_color 201 | c.local_name_color = self.local_name_color 202 | c.local_value_color = self.local_value_color 203 | c.local_len_color = self.local_len_color 204 | c.exception_color = self.exception_color 205 | c.exception_arg_color = self.exception_arg_color 206 | c.exception_file_color = self.exception_file_color 207 | c.syntax_error_color = self.syntax_error_color 208 | c.arrow_tail_color = self.arrow_tail_color 209 | c.arrow_head_color = self.arrow_head_color 210 | c.inner_exception_message = self.inner_exception_message 211 | c.inner_exception_separator = self.inner_exception_separator 212 | c.prefix = self.prefix 213 | c.infix = self.infix 214 | c.postfix = self.postfix 215 | c.reset_stdout = self.reset_stdout 216 | c.show_suppressed = self.show_suppressed 217 | return c 218 | 219 | __copy__ = copy 220 | 221 | 222 | config = PrettyErrorsConfig() 223 | default_config = PrettyErrorsConfig() 224 | 225 | 226 | def configure( 227 | always_display_bottom = None, 228 | arrow_head_character = None, 229 | arrow_tail_character = None, 230 | arrow_head_color = None, 231 | arrow_tail_color = None, 232 | code_color = None, 233 | display_arrow = None, 234 | display_link = None, 235 | display_locals = None, 236 | display_timestamp = None, 237 | display_trace_locals = None, 238 | exception_above = None, 239 | exception_arg_color = None, 240 | exception_below = None, 241 | exception_color = None, 242 | exception_file_color = None, 243 | filename_color = None, 244 | filename_display = None, 245 | full_line_newline = None, 246 | function_color = None, 247 | header_color = None, 248 | infix = None, 249 | inner_exception_message = None, 250 | inner_exception_separator = None, 251 | line_color = None, 252 | line_length = None, 253 | line_number_color = None, 254 | line_number_first = None, 255 | lines_after = None, 256 | lines_before = None, 257 | link_color = None, 258 | local_len_color = None, 259 | local_name_color = None, 260 | local_value_color = None, 261 | name = None, 262 | postfix = None, 263 | prefix = None, 264 | reset_stdout = None, 265 | separator_character = None, 266 | show_suppressed = None, 267 | stack_depth = None, 268 | syntax_error_color = None, 269 | timestamp_color = None, 270 | timestamp_function = None, 271 | top_first = None, 272 | trace_lines_after = None, 273 | trace_lines_before = None, 274 | truncate_code = None, 275 | truncate_locals = None 276 | ): 277 | """Configure settings governing how exceptions are displayed.""" 278 | config.configure( 279 | always_display_bottom = always_display_bottom, 280 | arrow_head_character = arrow_head_character, 281 | arrow_tail_character = arrow_tail_character, 282 | arrow_head_color = arrow_head_color, 283 | arrow_tail_color = arrow_tail_color, 284 | code_color = code_color, 285 | display_arrow = display_arrow, 286 | display_link = display_link, 287 | display_locals = display_locals, 288 | display_timestamp = display_timestamp, 289 | display_trace_locals = display_trace_locals, 290 | exception_above = exception_above, 291 | exception_arg_color = exception_arg_color, 292 | exception_below = exception_below, 293 | exception_color = exception_color, 294 | exception_file_color = exception_file_color, 295 | filename_color = filename_color, 296 | filename_display = filename_display, 297 | full_line_newline = full_line_newline, 298 | function_color = function_color, 299 | header_color = header_color, 300 | infix = infix, 301 | inner_exception_message = inner_exception_message, 302 | inner_exception_separator = inner_exception_separator, 303 | line_color = line_color, 304 | line_length = line_length, 305 | line_number_color = line_number_color, 306 | line_number_first = line_number_first, 307 | lines_after = lines_after, 308 | lines_before = lines_before, 309 | link_color = link_color, 310 | local_len_color = local_len_color, 311 | local_name_color = local_name_color, 312 | local_value_color = local_value_color, 313 | name = name, 314 | postfix = postfix, 315 | prefix = prefix, 316 | reset_stdout = reset_stdout, 317 | separator_character = separator_character, 318 | show_suppressed = show_suppressed, 319 | stack_depth = stack_depth, 320 | syntax_error_color = syntax_error_color, 321 | timestamp_color = timestamp_color, 322 | timestamp_function = timestamp_function, 323 | top_first = top_first, 324 | trace_lines_after = trace_lines_after, 325 | trace_lines_before = trace_lines_before, 326 | truncate_code = truncate_code, 327 | truncate_locals = truncate_locals 328 | ) 329 | 330 | 331 | def mono(): 332 | global RESET_COLOR 333 | RESET_COLOR = '' 334 | configure( 335 | name = "mono", 336 | infix = '\n---\n', 337 | line_number_first = True, 338 | code_color = '| ', 339 | exception_arg_color = '', 340 | exception_color = '', 341 | exception_file_color = '', 342 | filename_color = '', 343 | function_color = '', 344 | header_color = '', 345 | line_color = '> ', 346 | line_number_color = '', 347 | link_color = '', 348 | local_len_color = '', 349 | local_name_color = '= ', 350 | local_value_color = '', 351 | timestamp_color = '', 352 | arrow_head_color = '', 353 | arrow_tail_color = '', 354 | syntax_error_color = '' 355 | ) 356 | 357 | 358 | def whitelist(*paths): 359 | """If the whitelist has any entries, then only files which begin with 360 | one of its entries will be included in the stack trace. 361 | """ 362 | for path in paths: 363 | whitelist_paths.append(os.path.normpath(path).lower()) 364 | 365 | 366 | def blacklist(*paths): 367 | """Files which begin with a path on the blacklist will not be 368 | included in the stack trace. 369 | """ 370 | for path in paths: 371 | blacklist_paths.append(os.path.normpath(path).lower()) 372 | 373 | 374 | def pathed_config(configuration, *paths): 375 | """Use alternate configuration for files in the stack trace whose path 376 | begins with one of these paths.""" 377 | for path in paths: 378 | config_paths[os.path.normpath(path).lower()] = configuration 379 | 380 | 381 | 382 | class ExceptionWriter(): 383 | """ExceptionWriter class for outputing exceptions to the screen. 384 | Methods beginning 'write_' are the primary candidates for overriding. 385 | 386 | Inherit from this class, then set: 387 | pretty_errors.exception_writer = MyExceptionWriter() 388 | """ 389 | def __init__(self): 390 | self.config = None 391 | 392 | 393 | def get_terminal_width(self): 394 | """Width of terminal in characters.""" 395 | try: 396 | width = os.get_terminal_size()[0] 397 | return 79 if width <= 0 else width 398 | except Exception: 399 | return 79 400 | 401 | 402 | def get_line_length(self): 403 | """Calculated line length.""" 404 | if self.config.line_length <= 0: 405 | return self.get_terminal_width() 406 | else: 407 | return self.config.line_length 408 | 409 | 410 | def visible_length(self, s): 411 | """Visible length of string (i.e. without ansi escape sequences)""" 412 | return len(ansi_escape.sub('', s)) 413 | 414 | 415 | def output_text(self, texts): 416 | """Write list of texts to stderr. 417 | Use this function for all output. 418 | 419 | texts: a string or a list of strings 420 | """ 421 | if not isinstance(texts, (list, tuple)): 422 | texts = [texts] 423 | count = 0 424 | for text in texts: 425 | text = str(text) 426 | output_stderr.write(text) 427 | count += self.visible_length(text) 428 | line_length = self.get_line_length() 429 | if count == 0 or count % line_length != 0 or self.config.full_line_newline: 430 | output_stderr.write('\n') 431 | output_stderr.write(RESET_COLOR) 432 | if self.config.reset_stdout: 433 | sys.stdout.write(RESET_COLOR) 434 | 435 | 436 | def write_header(self): 437 | """Write stack trace header to screen. 438 | 439 | Should make use of: 440 | self.config.separator_character 441 | self.config.display_timestamp 442 | self.config.timestamp_function() 443 | self.config.header_color""" 444 | if not self.config.separator_character: return 445 | line_length = self.get_line_length() 446 | if self.config.display_timestamp: 447 | timestamp = str(self.config.timestamp_function()) 448 | separator = (line_length - len(timestamp)) * self.config.separator_character + timestamp 449 | else: 450 | separator = line_length * self.config.separator_character 451 | self.output_text('') 452 | self.output_text([self.config.header_color, separator]) 453 | 454 | 455 | def write_location(self, path, line, function): 456 | """Write location of frame to screen. 457 | 458 | Should make use of: 459 | self.config.filename_display 460 | self.config.filename_color 461 | self.config.line_number_color 462 | self.config.function_color 463 | self.config.line_number_first 464 | self.config.function_color 465 | self.config.display_link 466 | self.config.link_color 467 | """ 468 | line_number = str(line) + ' ' 469 | self.output_text('') 470 | if self.config.filename_display == FILENAME_FULL: 471 | filename = "" 472 | self.output_text([self.config.filename_color, path]) 473 | self.output_text([self.config.line_number_color, line_number, self.config.function_color, function]) 474 | else: 475 | if self.config.filename_display == FILENAME_EXTENDED: 476 | line_length = self.get_line_length() 477 | filename = path[-(line_length - len(line_number) - len(function) - 4):] 478 | if filename != path: 479 | filename = '...' + filename 480 | else: 481 | filename = os.path.basename(path) 482 | if self.config.line_number_first: 483 | self.output_text([ 484 | self.config.line_number_color, line_number, 485 | self.config.function_color, function + ' ', 486 | self.config.filename_color, filename 487 | ]) 488 | else: 489 | self.output_text([ 490 | self.config.filename_color, filename + ' ', 491 | self.config.line_number_color, line_number, 492 | self.config.function_color, function 493 | ]) 494 | if self.config.display_link: 495 | self.write_link(path, line) 496 | 497 | 498 | def write_link(self, filepath, line): 499 | """Write link of location to screen. Default version is clickable in VSCode. 500 | 501 | Should make use of: 502 | self.config.link_color 503 | """ 504 | 505 | self.output_text([self.config.link_color, '"%s", line %s' % (filepath, line)]) 506 | 507 | 508 | def write_code(self, filepath, line, module_globals, is_final, point_at = None): 509 | """Write frame code to screen. 510 | Parameters: 511 | filepath: path to code file 512 | line: line number in file 513 | module_globals: pass to linecache.getline() 514 | is_final: True if this is the last frame 515 | point_at: character position to point at 516 | 517 | Should make use of: 518 | self.config.lines_before 519 | self.config.lines_after 520 | self.config.trace_lines_before 521 | self.config.trace_lines_after 522 | self.config.truncate_code 523 | self.config.display_arrow 524 | self.config.arrow_head_character 525 | self.config.arrow_tail_character 526 | self.config.line_color 527 | self.config.code_color 528 | self.config.arrow_head_color 529 | self.config.arrow_tail_color 530 | self.config.syntax_error_color 531 | """ 532 | 533 | lines = [] 534 | if filepath == '': 535 | lines.append(str(line).rstrip()) 536 | line = target_line = start = end = 0 537 | else: 538 | if is_final: 539 | target_line = self.config.lines_before 540 | start = line - self.config.lines_before 541 | end = line + self.config.lines_after 542 | else: 543 | target_line = self.config.trace_lines_before 544 | start = line - self.config.trace_lines_before 545 | end = line + self.config.trace_lines_after 546 | 547 | if start < 1: 548 | target_line -= (1 - start) 549 | start = 1 550 | 551 | for i in range(start, end + 1): 552 | lines.append(linecache.getline(filepath, i, module_globals).rstrip()) 553 | 554 | min_lead = None 555 | for line in lines: 556 | if line.strip() == '': continue 557 | c = 0 558 | while c < len(line) and line[c] in (' ', '\t'): 559 | c += 1 560 | if min_lead is None or c < min_lead: 561 | min_lead = c 562 | if min_lead is None: 563 | min_lead = 0 564 | if min_lead > 0: 565 | lines = [line[min_lead:] for line in lines] 566 | 567 | line_length = self.get_line_length() 568 | 569 | for i, line in enumerate(lines): 570 | if i == target_line: 571 | color = self.config.line_color 572 | if point_at is not None: 573 | point_at -= (min_lead + 1) 574 | else: 575 | color = self.config.code_color 576 | color_length = self.visible_length(color) 577 | if self.config.truncate_code and len(line) + color_length > line_length: 578 | line = line[:line_length - color_length + 3] + '...' 579 | if i == target_line and point_at is not None: 580 | if point_at >= line_length: 581 | point_at = line_length - 1 582 | start_char = point_at 583 | while start_char > 0 and line[start_char - 1] not in (' ', '\t'): 584 | start_char -= 1 585 | end_char = point_at + 1 586 | while end_char < len(line) - 1 and line[end_char] not in (' ', '\t'): 587 | end_char += 1 588 | self.output_text([ 589 | color, line[:start_char], RESET_COLOR, 590 | self.config.syntax_error_color, line[start_char:end_char], RESET_COLOR, 591 | color, line[end_char:] 592 | ]) 593 | if self.config.display_arrow: 594 | self.output_text([ 595 | self.config.arrow_tail_color, self.config.arrow_tail_character * point_at, 596 | self.config.arrow_head_color, self.config.arrow_head_character 597 | ]) 598 | else: 599 | self.output_text([color, line]) 600 | 601 | return '\n'.join(lines) 602 | 603 | 604 | def exception_name(self, exception): 605 | """Name of exception.""" 606 | label = str(exception) 607 | if label.startswith(" 0: 621 | output = [ 622 | self.config.exception_color, self.exception_name(exception_type), ':\n', 623 | self.config.exception_arg_color, '\n'.join((str(x) for x in exception_value.args)) 624 | ] 625 | else: 626 | output = [self.config.exception_color, self.exception_name(exception_type)] 627 | 628 | for attr in ("filename", "filename2"): 629 | if hasattr(exception_value, attr): 630 | path = getattr(exception_value, attr) 631 | if path is not None: 632 | output.append('\n') 633 | output.append(self.config.exception_file_color) 634 | output.append(path) 635 | 636 | self.output_text(output) 637 | 638 | 639 | 640 | exception_writer = ExceptionWriter() 641 | 642 | 643 | 644 | def excepthook(exception_type, exception_value, traceback): 645 | "Replaces sys.excepthook to output pretty errors." 646 | 647 | writer = exception_writer 648 | writer.config = writer.default_config = config 649 | 650 | 651 | if (not writer.config.top_first and exception_value.__context__ 652 | and (not exception_value.__suppress_context__ or config.show_suppressed)): 653 | excepthook(type(exception_value.__context__), exception_value.__context__, exception_value.__context__.__traceback__) 654 | writer.config = writer.default_config 655 | if writer.config.inner_exception_message != None: 656 | if writer.config.inner_exception_separator: 657 | writer.write_header() 658 | output_stderr.write(writer.config.inner_exception_message) 659 | 660 | def check_for_pathed_config(path): 661 | writer.config = writer.default_config 662 | for config_path in config_paths: 663 | if path.startswith(config_path): 664 | writer.config = config_paths[config_path] 665 | break 666 | 667 | if traceback: 668 | tb = traceback 669 | while tb.tb_next != None: 670 | tb = tb.tb_next 671 | check_for_pathed_config(os.path.normpath(tb.tb_frame.f_code.co_filename).lower()) 672 | writer.default_config = writer.config 673 | 674 | writer.write_header() 675 | 676 | if writer.config.prefix != None: 677 | output_stderr.write(writer.config.prefix) 678 | 679 | syntax_error_info = None 680 | if exception_type == SyntaxError: 681 | syntax_error_info = exception_value.args[1] 682 | exception_value.args = [exception_value.args[0]] 683 | 684 | if writer.config.exception_above: 685 | writer.output_text('') 686 | writer.write_exception(exception_type, exception_value) 687 | 688 | if syntax_error_info: 689 | check_for_pathed_config(os.path.normpath(syntax_error_info[0]).lower()) 690 | writer.write_location(syntax_error_info[0], syntax_error_info[1], '') 691 | if(syntax_error_info[0] == ''): 692 | writer.write_code(syntax_error_info[0], syntax_error_info[3], [], True, syntax_error_info[2]) 693 | else: 694 | writer.write_code(syntax_error_info[0], syntax_error_info[1], [], True, syntax_error_info[2]) 695 | else: 696 | tracebacks = [] 697 | while traceback != None: 698 | path = os.path.normpath(traceback.tb_frame.f_code.co_filename).lower() 699 | if traceback.tb_next == None or (writer.config.always_display_bottom and tracebacks == []): 700 | tracebacks.append(traceback) 701 | else: 702 | if whitelist_paths: 703 | for white in whitelist_paths: 704 | if path.startswith(white): break 705 | else: 706 | traceback = traceback.tb_next 707 | continue 708 | for black in blacklist_paths: 709 | if path.startswith(black): break 710 | else: 711 | tracebacks.append(traceback) 712 | traceback = traceback.tb_next 713 | 714 | if writer.config.top_first: 715 | tracebacks.reverse() 716 | if writer.config.stack_depth > 0: 717 | if writer.config.always_display_bottom and len(tracebacks) > 1: 718 | tracebacks = tracebacks[:writer.config.stack_depth] + tracebacks[-1:] 719 | else: 720 | tracebacks = tracebacks[:writer.config.stack_depth] 721 | final = 0 722 | else: 723 | if writer.config.stack_depth > 0: 724 | if writer.config.always_display_bottom and len(tracebacks) > 1: 725 | tracebacks = tracebacks[:1] + tracebacks[-writer.config.stack_depth:] 726 | else: 727 | tracebacks = tracebacks[-writer.config.stack_depth:] 728 | final = len(tracebacks) - 1 729 | 730 | for count, traceback in enumerate(tracebacks): 731 | path = os.path.normpath(traceback.tb_frame.f_code.co_filename).lower() 732 | check_for_pathed_config(path) 733 | 734 | if writer.config.infix != None and count != 0: 735 | output_stderr.write(writer.config.infix) 736 | 737 | frame = traceback.tb_frame 738 | code = frame.f_code 739 | writer.write_location(code.co_filename, traceback.tb_lineno, code.co_name) 740 | code_string = writer.write_code(code.co_filename, traceback.tb_lineno, frame.f_globals, count == final) 741 | 742 | if (writer.config.display_locals and count == final) or (writer.config.display_trace_locals and count != final): 743 | local_variables = [(code_string.find(x), x) for x in frame.f_locals] 744 | local_variables.sort() 745 | local_variables = [x[1] for x in local_variables if x[0] >= 0] 746 | if local_variables: 747 | writer.output_text('') 748 | spacer = ': ' 749 | len_spacer = '... ' 750 | line_length = writer.get_line_length() 751 | for local in local_variables: 752 | value = str(frame.f_locals[local]) 753 | output = [writer.config.local_name_color, local, spacer, writer.config.local_value_color] 754 | if writer.config.truncate_locals and len(local) + len(spacer) + len(value) > line_length: 755 | length = '[' + str(len(value)) + ']' 756 | value = value[:line_length - (len(local) + len(spacer) + len(len_spacer) + len(length))] 757 | output += [value, len_spacer, writer.config.local_len_color, length] 758 | else: 759 | output += [value] 760 | writer.output_text(output) 761 | 762 | writer.config = writer.default_config 763 | 764 | if writer.config.exception_below: 765 | writer.output_text('') 766 | writer.write_exception(exception_type, exception_value) 767 | 768 | if writer.config.postfix != None: 769 | output_stderr.write(writer.config.postfix) 770 | 771 | if (writer.config.top_first and exception_value.__context__ and 772 | (not exception_value.__suppress_context__ or config.show_suppressed)): 773 | if writer.config.inner_exception_message != None: 774 | if writer.config.inner_exception_separator: 775 | writer.write_header() 776 | output_stderr.write(writer.config.inner_exception_message) 777 | excepthook(type(exception_value.__context__), exception_value.__context__, exception_value.__context__.__traceback__) 778 | 779 | 780 | 781 | location_expression = re.compile(r'.*File "([^"]*)", line ([0-9]+), in (.*)') 782 | 783 | class StdErr(): 784 | """Replaces sys.stderr in order to scrape it, when capturing stack trace is unavailable.""" 785 | def __init__(self): 786 | self.__in_exception = False 787 | self.__awaiting_code = False 788 | self.__awaiting_end = 0 789 | self.__frames = [] 790 | self.__point_at = None 791 | 792 | 793 | def __getattr__(self, name): 794 | return getattr(output_stderr, name) 795 | 796 | 797 | def __enter__(self, *args, **kwargs): 798 | return output_stderr.__enter__(*args, **kwargs) 799 | 800 | 801 | def __is_header(self, text): 802 | """Is text a traceback header?""" 803 | return text.startswith('Traceback (most recent call last):') 804 | 805 | 806 | def __is_outer_exception(self, text): 807 | """Is text a notification for an outer exception?""" 808 | return text.startswith('During handling of the above exception, another exception occurred:') 809 | 810 | 811 | def __get_location(self, text): 812 | """Extract location of exception. If it returns None then text was not a location identifier.""" 813 | location = location_expression.match(text) 814 | if location: 815 | return (location.group(1), int(location.group(2)), location.group(3)) 816 | else: 817 | return None 818 | 819 | 820 | def __write_exception(self): 821 | """Write exception, prettily.""" 822 | if not self.__frames: return 823 | 824 | writer = exception_writer 825 | writer.config = writer.default_config = config 826 | 827 | def check_for_pathed_config(path): 828 | writer.config = writer.default_config 829 | for config_path in config_paths: 830 | if path.startswith(config_path): 831 | writer.config = config_paths[config_path] 832 | break 833 | 834 | path, line_number, function = self.__frames[-1] 835 | check_for_pathed_config(os.path.normpath(path.lower())) 836 | writer.default_config = writer.config 837 | 838 | writer.write_header() 839 | 840 | if writer.config.prefix != None: 841 | output_stderr.write(writer.config.prefix) 842 | 843 | exception_type = self.__exception 844 | if self.__exception_args: 845 | class ExceptionValue(): 846 | def __init__(self): 847 | self.args = [] 848 | exception_value = ExceptionValue() 849 | if self.__exception_args.startswith('('): 850 | for arg in self.__exception_args[1:-1].split(', '): 851 | exception_value.args.append(arg) 852 | else: 853 | exception_value.args.append(self.__exception_args) 854 | else: 855 | exception_value = None 856 | 857 | syntax_error_info = None 858 | if self.__exception == 'SyntaxError': 859 | syntax_error_info = self.__frames[-1] 860 | syntax_error_info[2] = self.__point_at 861 | 862 | if writer.config.exception_above: 863 | writer.output_text('') 864 | writer.write_exception(exception_type, exception_value) 865 | 866 | if syntax_error_info: 867 | check_for_pathed_config(os.path.normpath(syntax_error_info[0]).lower()) 868 | writer.write_location(syntax_error_info[0], syntax_error_info[1], '') 869 | writer.write_code(syntax_error_info[0], syntax_error_info[1], [], True, syntax_error_info[2]) 870 | else: 871 | tracebacks = [] 872 | for i, frame in enumerate(self.__frames): 873 | path, line_number, function = frame 874 | path = os.path.normpath(path).lower() 875 | if (i == len(self.__frames) - 1) or (writer.config.always_display_bottom and tracebacks == []): 876 | tracebacks.append(frame) 877 | else: 878 | if whitelist_paths: 879 | for white in whitelist_paths: 880 | if path.startswith(white): break 881 | else: 882 | continue 883 | for black in blacklist_paths: 884 | if path.startswith(black): break 885 | else: 886 | tracebacks.append(frame) 887 | 888 | if writer.config.top_first: 889 | tracebacks.reverse() 890 | if writer.config.stack_depth > 0: 891 | if writer.config.always_display_bottom and len(tracebacks) > 1: 892 | tracebacks = tracebacks[:writer.config.stack_depth] + tracebacks[-1:] 893 | else: 894 | tracebacks = tracebacks[:writer.config.stack_depth] 895 | final = 0 896 | else: 897 | if writer.config.stack_depth > 0: 898 | if writer.config.always_display_bottom and len(tracebacks) > 1: 899 | tracebacks = tracebacks[:1] + tracebacks[-writer.config.stack_depth:] 900 | else: 901 | tracebacks = tracebacks[-writer.config.stack_depth:] 902 | final = len(tracebacks) - 1 903 | 904 | for count, traceback in enumerate(tracebacks): 905 | path, line_number, function = traceback 906 | normpath = os.path.normpath(path).lower() 907 | check_for_pathed_config(normpath) 908 | 909 | if writer.config.infix != None and count != 0: 910 | output_stderr.write(writer.config.infix) 911 | 912 | writer.write_location(path, line_number, function) 913 | writer.write_code(path, line_number, [], count == final) 914 | 915 | writer.config = writer.default_config 916 | 917 | if writer.config.exception_below: 918 | writer.output_text('') 919 | writer.write_exception(exception_type, exception_value) 920 | 921 | if writer.config.postfix != None: 922 | output_stderr.write(writer.config.postfix) 923 | 924 | 925 | def write(self, text): 926 | """Replaces sys.stderr.write, outputing pretty errors.""" 927 | if not self.__in_exception and not text.split(): 928 | output_stderr.write(text) 929 | else: 930 | for line in text.split('\n'): 931 | if self.__awaiting_end > 0: 932 | self.__awaiting_end -= 1 933 | if self.__awaiting_end == 0: 934 | self.__exception_args = line.strip() 935 | self.__in_exception = False 936 | self.__write_exception() 937 | elif self.__in_exception: 938 | if not line.strip(): 939 | pass 940 | elif self.__awaiting_code: 941 | self.__awaiting_code = False 942 | else: 943 | location = self.__get_location(line) 944 | if location: 945 | self.__frames.append(location) 946 | self.__awaiting_code = True 947 | elif line.strip() == '^': 948 | self.__point_at = len(line) - 2 949 | else: # end of traceback 950 | if ': ' in line: 951 | self.__exception, self.__exception_args = line.strip().split(': ', 1) 952 | self.__in_exception = False 953 | self.__write_exception() 954 | else: 955 | self.__awaiting_end = 2 956 | self.__exception = line.strip() 957 | elif self.__is_header(line): 958 | self.__in_exception = True 959 | self.__awaiting_code = False 960 | self.__frames = [] 961 | elif self.__is_outer_exception(line): 962 | if config.inner_exception_message != None: 963 | if config.inner_exception_separator: 964 | exception_writer.write_header() 965 | output_stderr.write(config.inner_exception_message) 966 | else: 967 | output_stderr.write(line) 968 | 969 | 970 | 971 | def replace_stderr(force = False): 972 | """Replace sys.stderr, for cases where standard use with activate() does not work.""" 973 | if (force or not interactive_tty_only or terminal_is_interactive): 974 | sys.stderr = StdErr() 975 | 976 | 977 | def activate(): 978 | """Set sys.excepthook to use pretty errors.""" 979 | sys.excepthook = excepthook 980 | 981 | 982 | if active and (not interactive_tty_only or terminal_is_interactive): 983 | activate() 984 | 985 | 986 | 987 | if __name__ == "__main__": 988 | configure( 989 | filename_display = FILENAME_EXTENDED, 990 | line_number_first = True, 991 | display_link = True, 992 | lines_before = 5, 993 | lines_after = 2, 994 | line_color = RED + '> ' + default_config.line_color, 995 | code_color = ' ' + default_config.line_color, 996 | truncate_code = True, 997 | inner_exception_separator=True, 998 | inner_exception_message = MAGENTA + "\n During handling of the above exception, another exception occurred:\n", 999 | display_locals = True 1000 | ) 1001 | blacklist('c:/python') 1002 | try: 1003 | myval = [1,2] 1004 | print(myval[3]) 1005 | except: 1006 | a = "C" * "B" 1007 | -------------------------------------------------------------------------------- /pretty_errors/__main__.py: -------------------------------------------------------------------------------- 1 | import pretty_errors, sys, os, re, site 2 | if '-h' in sys.argv or '--help' in sys.argv or '/?' in sys.argv: 3 | print("""\ 4 | Run without options to use interactive mode, otherwise: 5 | 6 | python -m pretty_errors [-m] <-u|-s> [-p] OR <-f> OR <-c> 7 | 8 | -m = monochrome (do not use color escape codes in output of this menu) 9 | -u = install in default user location 10 | -s = install in default system location 11 | -p = with above, install in pretty_errors.pth instead of *customize.py 12 | -f = find where pretty_errors is installed 13 | -c = clean pretty_errors from python startup (do so before uninstalling)""") 14 | 15 | else: 16 | find = ('-f' in sys.argv) 17 | add_to_user = ('-u' in sys.argv) 18 | add_to_site = ('-s' in sys.argv) 19 | pth = ('-p' in sys.argv) 20 | clean = ('-c' in sys.argv) 21 | mono = ('-m' in sys.argv) 22 | 23 | if not mono: 24 | reset_color = pretty_errors.RESET_COLOR 25 | file_color = pretty_errors.BRIGHT_CYAN 26 | message_color = pretty_errors.BRIGHT_RED 27 | key_color = pretty_errors.YELLOW 28 | error_color = pretty_errors.RED 29 | else: 30 | reset_color = '' 31 | file_color = '' 32 | message_color = '' 33 | key_color = '' 34 | error_color = '' 35 | 36 | check = re.compile(r'^\s*import\s+\bpretty_errors\b', re.MULTILINE) 37 | 38 | in_virtualenv = 'VIRTUAL_ENV' in os.environ 39 | # in virtualenv, `site` has no attribute `getsitepackages` or `getusersitepackages` 40 | if in_virtualenv: 41 | def getsitepackages(): 42 | if os.path.sep == "\\": 43 | sep = "\\\\" 44 | env_path = os.environ['VIRTUAL_ENV'].replace("\\", "\\\\") 45 | else: 46 | sep = os.path.sep 47 | env_path = os.environ['VIRTUAL_ENV'] 48 | pattern1 = re.compile(r'^%s$' % sep.join([env_path, 'lib', 'python[0-9.]+', 'site-packages'])) 49 | pattern2 = re.compile(r'^%s$' % sep.join([env_path, 'lib', 'site-packages'])) 50 | paths = [path for path in set(sys.path) if pattern1.search(path) or pattern2.search(path)] 51 | return paths 52 | 53 | def getusersitepackages(): 54 | return [] 55 | else: 56 | getsitepackages = site.getsitepackages 57 | 58 | def getusersitepackages(): 59 | return [site.getusersitepackages()] 60 | 61 | def getallsitepackages(): 62 | return getsitepackages() + getusersitepackages() 63 | 64 | 65 | def readfile(path): 66 | try: 67 | return ''.join((x for x in open(path))) 68 | except IOError: 69 | return '' 70 | 71 | 72 | def find_install(quiet = False): 73 | found = False 74 | for path in getallsitepackages(): 75 | for filename in 'usercustomize.py', 'sitecustomize.py', 'pretty_errors.pth': 76 | filepath = os.path.join(path, filename) 77 | if check.search(readfile(filepath)): 78 | if not found: 79 | print(message_color + '\n\npretty_errors found in:' + file_color) 80 | found = True 81 | print(filepath) 82 | print(reset_color) 83 | if not found and not quiet: 84 | print(message_color + '\npretty_errors not currently installed in any expected locations.\n' + reset_color) 85 | return found 86 | 87 | def clean_install(): 88 | files_to_edit = [] 89 | files_to_remove = [] 90 | for path in getallsitepackages(): 91 | for filename in 'usercustomize.py', 'sitecustomize.py', 'pretty_errors.pth': 92 | filepath = os.path.join(path, filename) 93 | data = readfile(filepath).strip() 94 | if check.search(data): 95 | if filename.endswith('.pth') or ( 96 | data.startswith('### BEGIN PRETTY ERRORS') and 97 | data.endswith('### END PRETTY ERRORS')): 98 | files_to_remove.append(filepath) 99 | else: 100 | files_to_edit.append(filepath) 101 | if not files_to_edit and not files_to_remove: 102 | print(message_color + '\npretty_errors not currently installed in any expected locations.' + reset_color) 103 | return 104 | if files_to_remove: 105 | print('\nAttempting to remove the following files:\n') 106 | for filepath in files_to_remove: 107 | print(file_color + filepath + reset_color, end='... ') 108 | try: 109 | os.remove(filepath) 110 | except Exception as e: 111 | print(error_color + 'ERR\n' + str(e) + reset_color) 112 | else: 113 | print('OK') 114 | if files_to_edit: 115 | print('\n\nFound entry for pretty_errors in the following files.\n' + 116 | message_color + 117 | 'Please edit and remove the relevant section:\n') 118 | for filepath in files_to_edit: 119 | print(file_color + filepath + reset_color) 120 | 121 | if find: 122 | find_install() 123 | sys.exit(0) 124 | elif clean: 125 | clean_install() 126 | sys.exit(0) 127 | 128 | if add_to_user: 129 | if in_virtualenv: 130 | print('You are now in virtualenv, which has sitepackage but no user sitepackages, ' + 131 | message_color + '`-u`' + reset_color + ' is incompatible with virtualenv.') 132 | print('Please use ' + message_color + '`-s`' + reset_color + ' instead.') 133 | sys.exit(1) 134 | path = getusersitepackages()[0] 135 | filename = 'pretty_errors.pth' if pth else 'usercustomize.py' 136 | elif add_to_site: 137 | path = getsitepackages()[0] 138 | filename = 'pretty_errors.pth' if pth else 'sitecustomize.py' 139 | else: 140 | 141 | found = find_install(True) 142 | 143 | def prompt(key, caption, is_path = False): 144 | if is_path: 145 | print(key_color + key + reset_color + ': ' + file_color + caption + reset_color) 146 | else: 147 | print(key_color + key + reset_color + ': ' + caption + reset_color) 148 | 149 | def get_choice(query, choices, default = None): 150 | options = {} 151 | for i in range(len(choices)): 152 | options[str(i + 1)] = i 153 | while True: 154 | print() 155 | print(' ' + query) 156 | print() 157 | if found: 158 | prompt('C', 'Clean startup files (do so before uninstalling pretty_errors)') 159 | for option, choice in enumerate(choices): 160 | prompt('%d' % (option + 1), choice, True) 161 | prompt('0', 'Exit') 162 | if default is None: 163 | print('\nOption: ', end='') 164 | else: 165 | print('\nOption: [default: ' + message_color + str(default + 1) + reset_color + '] ', end='') 166 | choice = input() 167 | if choice == '' and default is not None: 168 | choice = str(default + 1) 169 | if choice == '0': 170 | sys.exit(0) 171 | elif choice.lower() == 'c': 172 | clean_install() 173 | sys.exit(0) 174 | elif choice in options: 175 | return options[choice] 176 | 177 | formatting = {'file': file_color, 'msg': message_color, 'reset': reset_color, 'key': key_color} 178 | print(""" 179 | To have pretty_errors be used when you run any python file you may add it to your\ 180 | %(file)s usercustomize.py %(reset)s(user level) or\ 181 | %(file)s sitecustomize.py %(reset)s(system level), or to\ 182 | %(file)s pretty_errors.pth%(reset)s. 183 | 184 | (just hit %(key)s%(reset)s to accept the defaults if you are unsure) 185 | """ % formatting) 186 | 187 | paths = getallsitepackages() 188 | path = paths[get_choice('Choose folder to install into:', paths, -1 if found else len(paths) - 1)] 189 | 190 | if path in getsitepackages(): 191 | filenames = ['sitecustomize.py', 'pretty_errors.pth'] 192 | else: 193 | filenames = ['usercustomize.py', 'pretty_errors.pth'] 194 | filename = filenames[get_choice('Choose file to install into:', filenames, 0)] 195 | 196 | if filename.endswith('.pth'): 197 | output = (os.path.dirname(os.path.dirname(os.path.normpath(__file__))) + 198 | '\nimport pretty_errors; ' + 199 | '#pretty_errors.configure() ' + 200 | '# keep on one line, for options see ' + 201 | 'https://github.com/onelivesleft/PrettyErrors/blob/master/README.md' 202 | ) 203 | else: 204 | output = [] 205 | output.append(''' 206 | 207 | ### BEGIN PRETTY ERRORS 208 | 209 | # pretty-errors package to make exception reports legible. 210 | # v%s generated this config: newer version may have added methods/options not present! 211 | 212 | try: 213 | import pretty_errors 214 | except ImportError: 215 | print( 216 | 'You have uninstalled pretty_errors but it is still present in your python startup.' + 217 | ' Please remove its section from file:\\n ' + __file__ + '\\n' 218 | ) 219 | else: 220 | pass 221 | 222 | # Use if you do not have a color terminal: 223 | #pretty_errors.mono() 224 | 225 | # Use if you are using a framework which is handling all the exceptions before pretty_errors can: 226 | #if pretty_errors.active: 227 | # pretty_errors.replace_stderr() 228 | 229 | # Use to hide frames whose file begins with these paths: 230 | #pretty_errors.blacklist('/path/to/blacklist', '/other/path/to/blacklist', ...) 231 | 232 | # Use to only show frames whose file begins with these paths: 233 | #pretty_errors.whitelist('/path/to/whitelist', '/other/path/to/whitelist', ...) 234 | 235 | # Use to selectively set a config based on the path to the code of the current frame. 236 | #alternate_config = pretty_errors.config.copy() 237 | #pretty_errors.pathed_config(alternate_config, '/use/alternate/for/this/path') 238 | 239 | # Use to configure output: Uncomment each line to change that setting. 240 | """pretty_errors.configure( 241 | ''' % pretty_errors.__version__) 242 | 243 | options = [] 244 | colors = [] 245 | parameters = [] 246 | max_length = 0 247 | for option in dir(pretty_errors.config): 248 | if len(option) > max_length: 249 | max_length = len(option) 250 | if (option not in ('configure', 'mono', 'copy') and 251 | not option.startswith('_')): 252 | if option.endswith('_color'): 253 | colors.append(option) 254 | elif option != 'name': 255 | options.append(option) 256 | indent = ' ' 257 | def prefix(s): 258 | return indent + '#' + s.ljust(max_length) + ' = ' 259 | for option in sorted(options): 260 | if option == 'filename_display': 261 | parameters.append(prefix(option) + 'pretty_errors.FILENAME_COMPACT, # FILENAME_EXTENDED | FILENAME_FULL') 262 | elif option == 'timestamp_function': 263 | parameters.append(prefix(option) + 'time.perf_counter') 264 | else: 265 | parameters.append(prefix(option) + repr(getattr(pretty_errors.config, option))) 266 | for option in sorted(colors): 267 | parameters.append(prefix(option) + repr(getattr(pretty_errors.config, option))) 268 | parameters.append('\n' + indent + 'name = "custom" # name it whatever you want') 269 | output.append(',\n'.join(parameters)) 270 | output.append('\n )"""\n') 271 | output.append('### END PRETTY ERRORS\n') 272 | output = '\n'.join(output) 273 | 274 | print('\n--------------') 275 | 276 | filepath = os.path.join(path, filename) 277 | if check.search(readfile(filepath)): 278 | print('\npretty_errors already present in:\n' + file_color + '\n' + filepath + 279 | '\n' + reset_color + '\nEdit it to set config options.\n') 280 | sys.exit(0) 281 | 282 | try: 283 | os.makedirs(path) 284 | except Exception: 285 | pass 286 | try: 287 | out = open(filepath, 'a') 288 | out.write(output) 289 | out.close() 290 | except Exception: 291 | print('\nFailed to write to:\n' + filepath) 292 | else: 293 | print('\npretty_errors added to:\n' + file_color + '\n' + filepath + '\n' + reset_color + '\nEdit it to set config options.\n') 294 | if filepath.endswith('.pth'): 295 | print(error_color + '\n*** Delete this file when you uninstall pretty_errors! ***\n' + reset_color) 296 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pretty_errors", 8 | version="1.2.25", # remember to also update __version__ in __init__.py! 9 | author="Iain King", 10 | author_email="iain.king@gmail.com", 11 | description="Prettifies Python exception output to make it legible.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/onelivesleft/PrettyErrors/", 15 | packages=setuptools.find_packages(), 16 | install_requires=[ 17 | "colorama", 18 | ], 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 2", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | ) 26 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import pretty_errors 2 | 3 | pretty_errors.configure( 4 | separator_character = '*', 5 | filename_display = pretty_errors.FILENAME_FULL, 6 | lines_before=10, 7 | code_color = pretty_errors.default_config.line_color, 8 | stack_depth=2, 9 | exception_file_color=pretty_errors.BRIGHT_BLUE 10 | ) 11 | irrelevant = pretty_errors.config.copy() 12 | irrelevant.line_color = irrelevant.code_color = irrelevant.filename_color = irrelevant.function_color = irrelevant.line_number_color = ( 13 | pretty_errors.GREY 14 | ) 15 | pretty_errors.pathed_config(irrelevant, 'c:/python', 'c:/users') 16 | 17 | 18 | import os 19 | os.rename('nofile', 'stillnofile') 20 | -------------------------------------------------------------------------------- /upload.bat: -------------------------------------------------------------------------------- 1 | rem twine upload --repository-url https://test.pypi.org/legacy/ dist/* 2 | twine upload dist/* 3 | --------------------------------------------------------------------------------