├── .flake8 ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ae.bat ├── basic_text.out ├── demos ├── adder.py └── hello.py ├── friendly ├── __init__.py ├── __main__.py ├── console.py ├── idle │ ├── __init__.py │ ├── get_syntax.py │ ├── main.py │ └── patch_source_cache.py ├── idle_writer.py ├── ipython.py ├── ipython_common │ ├── __init__.py │ ├── excepthook.py │ └── settings.py ├── jupyter.py ├── locales │ ├── es │ │ └── LC_MESSAGES │ │ │ ├── friendly_es.mo │ │ │ └── friendly_es.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── friendly_fr.mo │ │ │ └── friendly_fr.po │ ├── friendly.pot │ └── ru │ │ └── LC_MESSAGES │ │ ├── friendly_ru.mo │ │ └── friendly_ru.po ├── make_pot.bat ├── mu │ ├── __init__.py │ ├── repl.py │ └── runner.py ├── my_gettext.py ├── py.typed ├── rich_console_helpers.py ├── rich_formatters.py ├── settings.py └── theme │ ├── __init__.py │ ├── colours.py │ ├── friendly_pygments.py │ ├── friendly_rich.py │ └── patch_tb_lexer.py ├── friendly_logo.png ├── images ├── explain.png ├── friendly_logo.png └── jb_beam.png ├── manifest.in ├── pypi_upload.bat ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── basic_text.out └── test_simple.py ├── upgrade_all.bat └── upgrade_ft.bat /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | # see https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes 4 | ignore = E203, E266, E402, E501, W503 5 | exclude = 6 | # No need to traverse our git directory 7 | .git/*, 8 | # There's no value in checking cache directories 9 | __pycache__, 10 | # documentation, to be ignored 11 | docs/*, 12 | # This contains our tests directory 13 | tests/* 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: aroberge 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. Include 12 | 13 | * friendly's version 14 | * friendly_traceback's version 15 | * Python's version 16 | * How friendly was used: 17 | * Using friendly's console 18 | * What operating system and terminal are you using? 19 | * In a Jupyter notebook (using `from friendly.jupyter import ...`) 20 | * How were you running your notebook (which browser or IDE) 21 | * In an IPython console (using `from friendly.ipython import ...`) 22 | * With Mu (using `from friendly.mu import ...`); specify if it was in the REPL or not 23 | * With IDLE (using `from friendly.idle import ...`). 24 | * Did you consider using friendly_idle? 25 | 26 | **To Reproduce** 27 | 28 | Try to provide a short example showing how to reproduce the problem. 29 | 30 | 31 | **Screenshots** 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: aroberge 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | db.sqlite3-journal 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | Untitled*.ipynb 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # pipenv 85 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 86 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 87 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 88 | # install all needed dependencies. 89 | #Pipfile.lock 90 | 91 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 92 | __pypackages__/ 93 | 94 | # Celery stuff 95 | celerybeat-schedule 96 | celerybeat.pid 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | venv*/ 110 | 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | 131 | .idea/ 132 | 133 | ignore*.py 134 | example*.py 135 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 22.6.0 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | exclude: ^tests/ 8 | - repo: https://gitlab.com/pycqa/flake8 9 | rev: 3.9.2 10 | hooks: 11 | - id: flake8 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | andre.roberge@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | The simplest but most important way in which you can contribute is by filing an issue. 2 | This issue could be a suggestion for improvements, pointing out mistakes either in 3 | the documentation or in the information provided by **Friendly**, or finding bugs. 4 | 5 | If you speak a language other than English or French and feel ambitious, you might 6 | want to work on translations into your own language. 7 | 8 | ## Please read before creating a pull request 9 | 10 | In most cases, you do not need to create a pull request. 11 | If you wish to contribute using a pull request, before doing so **please file an issue** 12 | (unless you have contributed before and are familiar with this project.) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 friendly-traceback 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![friendly-traceback logo](images/friendly_logo.png) 2 | # friendly/friendly-traceback 3 | 4 | - **friendly_traceback**: Helps understand Python traceback 5 | - **friendly**: Prettier version of the above with some additional enhancements. 6 | 7 | This code repository is for **friendly**. 8 | 9 | Unless specified otherwise, from here on, **Friendly** will refer to both 10 | **friendly** and **friendly_traceback** 11 | 12 | ## Description 13 | 14 | Created with Python beginners in mind, but also useful for experienced users, 15 | **Friendly** replaces standard tracebacks 16 | by something easier to understand, translatable into various languages. 17 | Currently, the information provided by **Friendly** is only available 18 | in two languages: English and French. 19 | 20 | The additional information provided by **Friendly** includes 21 | `why()` a certain exception occurred, 22 | `what()` it means, exactly `where()` it occurred including 23 | the value of relevant variables, and 24 | [more](https://aroberge.github.io/friendly-traceback-docs/docs/html/). 25 | 26 | ## Installation 27 | 28 | Most users should install **friendly** instead of **friendly_traceback**, 29 | 30 | ``` 31 | python -m pip install friendly 32 | ``` 33 | 34 | This needs to be done from a terminal. 35 | In the command shown above, 36 | `python` refers to whatever you need to type to invoke your 37 | favourite Python interpreter. 38 | It could be `python`, `python3`, `py -3.8`, etc. 39 | 40 | For some special cases, including 41 | using a specialized editor like [Mu](https://codewith.mu) that has its own way 42 | of installing Python packages, please consult the documentation. 43 | 44 | ## Documentation 45 | 46 | [The documentation is available by clicking here.](https://friendly-traceback.github.io/docs/index.html) 47 | 48 | ## Example 49 | 50 | The following example illustrates the information that can 51 | be provided by **Friendly**. 52 | 53 | First, we show the output of **friendly-traceback** 54 | 55 | ``` 56 | Traceback (most recent call last): 57 | File "", line 1, in 58 | test() 59 | File "", line 2, in test 60 | a = cost(pi) 61 | NameError: name 'cost' is not defined 62 | 63 | Did you mean `cos`? 64 | 65 | A `NameError` exception indicates that a variable or 66 | function name is not known to Python. 67 | Most often, this is because there is a spelling mistake. 68 | However, sometimes it is because the name is used 69 | before being defined or given a value. 70 | 71 | In your program, `cost` is an unknown name. 72 | Instead of writing `cost`, perhaps you meant one of the following: 73 | * Global scope: `cos`, `cosh`, `acos` 74 | 75 | Execution stopped on line 1 of file `''`. 76 | 77 | -->1: test() 78 | 79 | test: 80 | 81 | Exception raised on line 2 of file `''`. 82 | 83 | 1: def test(): 84 | -->2: a = cost(pi) 85 | ^^^^ 86 | 87 | global pi: 3.141592653589793 88 | ``` 89 | 90 | Next, the same output shown as a screen capture when using **friendly**. 91 | ![Screen capture of the above example](images/explain.png) 92 | 93 | ## Projects using Friendly 94 | 95 | friendly/friendly-traceback is used by: 96 | 97 | * [HackInScience](https://hackinscience.org) 98 | * [futurecoder](https://futurecoder.io) 99 | * [CodeGrade](https://www.codegrade.com/blog/friendly-better-error-messages-for-python) 100 | * [ddebug](https://github.com/matan-h/ddebug) 101 | 102 | Feel free to contact me to add your project to this list. 103 | 104 | ## Contribute 105 | 106 | Contribute by making suggestions for improvements, pointing out mistakes either in 107 | the documentation or in the information provided by **Friendly**, or finding bugs. 108 | 109 | If you speak a language other than English or French and feel ambitious, you might 110 | want to work on translations into your own language. 111 | 112 | For more details, see [CONTRIBUTING](CONTRIBUTING.md) 113 | 114 | ## License: MIT 115 | 116 | For more details, see [LICENSE](LICENSE). 117 | 118 | Some ideas were adopted from 119 | [DidYouMean-Python (aka BetterErrorMessages)](https://github.com/SylvainDe/DidYouMean-Python) 120 | by Sylvain Desodt, a project that is also using the MIT license; as of October 2021, 121 | that particular project is no longer maintained. 122 | 123 | ## Code of Conduct 124 | 125 | In short: be respectful of everyone. 126 | 127 | For more details, see [Code of Conduct](CODE_OF_CONDUCT.md) 128 | 129 | ## JetBrains support 130 | 131 | We graciously acknowledge the support of [JetBrains]( 132 | https://www.jetbrains.com/community/opensource/?from=friendly-traceback) 133 | which enables us to use the professional version 134 | of PyCharm for developing **Friendly**. 135 | 136 | [![JetBrains](images/jb_beam.png)]( 137 | https://www.jetbrains.com/community/opensource/?from=friendly-traceback) 138 | -------------------------------------------------------------------------------- /ae.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | REM Default is Python 310 3 | 4 | if "%1"=="3.6" goto py_36 5 | if "%1"=="3.7" goto py_37 6 | if "%1"=="3.8" goto py_38 7 | if "%1"=="3.9" goto py_39 8 | if "%1"=="3.10" goto py_310 9 | if "%1"=="3.11" goto py_311 10 | if "%1"=="ipython" goto ipython 11 | 12 | :py_310 13 | venv-friendly-3.10\scripts\activate 14 | goto end 15 | 16 | :py_36 17 | venv-friendly-3.6\scripts\activate 18 | goto end 19 | 20 | :py_37 21 | venv-friendly-3.7\scripts\activate 22 | goto end 23 | 24 | :py_38 25 | venv-friendly-3.8\scripts\activate 26 | goto end 27 | 28 | :py_39 29 | venv-friendly-3.9\scripts\activate 30 | goto end 31 | 32 | :py_311 33 | venv-friendly-3.11\scripts\activate 34 | goto end 35 | 36 | :ipython 37 | venv-friendly-ipython\scripts\activate 38 | goto end 39 | 40 | :end 41 | -------------------------------------------------------------------------------- /basic_text.out: -------------------------------------------------------------------------------- 1 | 2 | ┌───────────────────────────────── Traceback ─────────────────────────────────┐ 3 | │ Traceback (most recent call last):  │ 4 | │  File "HOME:\github\friendly\tests\test_simple.py", line 11, in test_basic │ 5 | │  a = b  │ 6 | │ NameError: name 'b' is not defined  │ 7 | │ │ 8 | │ A NameError exception indicates that a variable or function name is not │ 9 | │ known to Python. Most often, this is because there is a spelling mistake. │ 10 | │ However, sometimes it is because the name is used before being defined or │ 11 | │ given a value. │ 12 | │ │ 13 | │ In your program, no object with the name b exists. I have no additional │ 14 | │ information for you. │ 15 | │ │ 16 | │ Exception raised on line 11 of file  │ 17 | │ 'HOME:\github\friendly\tests\test_simple.py'. │ 18 | │ │ 19 | │  8| console = session.console │ 20 | │  9| with console.capture() as capture: │ 21 | │  10| try: │ 22 | │  > 11| a = b │ 23 | │  12| except NameError: │ 24 | └─────────────────────────────────────────────────────────────────────────────┘ 25 | 26 | -------------------------------------------------------------------------------- /demos/adder.py: -------------------------------------------------------------------------------- 1 | # Demonstration of a program that uses command line arguments 2 | import argparse 3 | 4 | parser = argparse.ArgumentParser() 5 | parser.add_argument("numbers", nargs="*", help="List of numbers to add.") 6 | parser.add_argument("--to_int", help="Converts the sum to integer", action="store_true") 7 | 8 | 9 | total = 0 10 | args = parser.parse_args() 11 | 12 | for number in args.numbers: 13 | total += float(number) 14 | 15 | if int(total) == total and args.to_int: 16 | total = int(total) 17 | 18 | print("The sum is", total) 19 | -------------------------------------------------------------------------------- /demos/hello.py: -------------------------------------------------------------------------------- 1 | print("\nHello world!") 2 | 3 | if __name__ == "__main__": 4 | print("Running as main!") 5 | -------------------------------------------------------------------------------- /friendly/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | friendly.__init__.py 3 | ============================== 4 | 5 | With the exceptions of the functions that are specific to the console, 6 | this module contains all the functions that are part of the public API. 7 | While Friendly is still considered to be in alpha stage, 8 | we do attempt to avoid creating incompatibility for the functions 9 | here when introducing changes. 10 | 11 | The goal is to be even more careful to avoid introducing incompatibilities 12 | when reaching beta stage, and planning to be always backward compatible 13 | starting at version 1.0 -- except possibly for the required minimal 14 | Python version. 15 | 16 | Friendly is currently compatible with Python versions 3.6 17 | or newer. 18 | 19 | If you find that some additional functionality would be useful to 20 | have as part of the public API, please let us know. 21 | """ 22 | import os 23 | import sys 24 | 25 | valid_version = sys.version_info >= (3, 6, 1) 26 | 27 | if not valid_version: # pragma: no cover 28 | # Rich does not support 3.6.0 29 | print("Python 3.6.1 or newer is required.") 30 | sys.exit() 31 | 32 | del valid_version 33 | __version__ = "0.7.21" 34 | 35 | 36 | # =========================================== 37 | 38 | import inspect 39 | from pathlib import Path 40 | 41 | from .my_gettext import current_lang 42 | from . import rich_formatters, theme, settings 43 | 44 | from friendly_traceback import ( 45 | about_warnings, 46 | editors_helpers, 47 | exclude_directory_from_traceback, 48 | set_stream, 49 | ) 50 | 51 | from friendly_traceback import explain_traceback as ft_explain_traceback 52 | from friendly_traceback import install as ft_install 53 | from friendly_traceback import set_formatter as ft_set_formatter 54 | from friendly_traceback import set_lang 55 | from friendly_traceback import __version__ as ft_version 56 | 57 | from friendly_traceback.config import session 58 | 59 | 60 | # The following are not used here, and simply made available directly for convenience 61 | from friendly_traceback import ( # noqa 62 | exclude_file_from_traceback, 63 | get_include, 64 | get_output, 65 | get_stream, 66 | set_include, 67 | friendly_exec, 68 | hide_secrets, 69 | add_other_set_lang, 70 | ) 71 | 72 | about_warnings.enable_warnings() 73 | exclude_directory_from_traceback(os.path.dirname(__file__)) 74 | get_lang = settings.get_lang 75 | 76 | 77 | def install(lang=None, formatter=None, redirect=None, include="explain", _debug=None): 78 | """ 79 | Replaces ``sys.excepthook`` by friendly's own version. 80 | Intercepts, and can provide an explanation for all Python exceptions except 81 | for ``SystemExist`` and ``KeyboardInterrupt``. 82 | 83 | The optional arguments are: 84 | 85 | lang: language to be used for translations. If not available, 86 | English will be used as a default. 87 | 88 | formatter: if desired, sets a specific formatter to use. 89 | 90 | redirect: stream to be used to send the output. 91 | The default is sys.stderr 92 | 93 | include: controls the amount of information displayed. 94 | See set_include() for details. 95 | """ 96 | # Note: need "explain" since there is no interaction possible with install 97 | if lang is None: 98 | lang = get_lang() 99 | set_formatter(formatter=formatter) 100 | ft_install(lang=lang, redirect=redirect, include=include, _debug=_debug) 101 | 102 | 103 | def explain_traceback(formatter=None, redirect=None): 104 | """Replaces a standard traceback by a friendlier one, giving more 105 | information about a given exception than a standard traceback. 106 | Note that this excludes ``SystemExit`` and ``KeyboardInterrupt`` 107 | which are re-raised. 108 | 109 | If no formatter is specified, the default one will be used. 110 | 111 | By default, the output goes to ``sys.stderr`` or to some other stream 112 | set to be the default by another API call. However, if:: 113 | 114 | redirect = some_stream 115 | 116 | is specified, the output goes to that stream, but without changing 117 | the global settings. 118 | 119 | If the string ``"capture"`` is given as the value for ``redirect``, the 120 | output is saved and can be later retrieved by ``get_output()``. 121 | """ 122 | set_formatter(formatter=formatter) 123 | ft_explain_traceback(redirect=redirect) 124 | 125 | 126 | def run( 127 | filename, 128 | lang=None, 129 | include=None, 130 | args=None, 131 | console=True, 132 | formatter=None, 133 | redirect=None, 134 | background=None, 135 | ipython_prompt=True, 136 | ): 137 | """Given a filename (relative or absolute path) ending with the ".py" 138 | extension, this function uses the 139 | more complex ``exec_code()`` to run a file. 140 | 141 | If console is set to ``False``, ``run()`` returns an empty dict 142 | if a ``SyntaxError`` was raised, otherwise returns the dict in 143 | which the module (``filename``) was executed. 144 | 145 | If console is set to ``True`` (the default), the execution continues 146 | as an interactive session in a Friendly console, with the module 147 | dict being used as the locals dict. 148 | 149 | Other arguments include: 150 | 151 | ``lang``: language used; currently only ``'en'`` (default) and ``'fr'`` 152 | are available. 153 | 154 | ``include``: specifies what information is to be included if an 155 | exception is raised; the default is ``"friendly_tb"`` if console 156 | is set to ``True``, otherwise it is ``"explain"`` 157 | 158 | ``args``: strings tuple that is passed to the program as though it 159 | was run on the command line as follows:: 160 | 161 | python filename.py arg1 arg2 ... 162 | 163 | ``use_rich``: ``False`` by default. Set it to ``True`` if Rich is available 164 | and the environment supports it. 165 | 166 | ``theme``: Theme to be used with Rich. Currently, only ``"dark"``, 167 | the default, and ``"light"`` are available. ``"light"`` is meant for 168 | light coloured background and has not been extensively tested. 169 | """ 170 | _ = current_lang.translate 171 | if include is None: 172 | include = "friendly_tb" if console else "explain" 173 | if args is not None: 174 | sys.argv = [filename, *list(args)] 175 | else: 176 | filename = Path(filename) 177 | if not filename.is_absolute(): 178 | frame = inspect.stack()[1] 179 | # This is the file from which run() is called 180 | run_filename = Path(frame[0].f_code.co_filename) 181 | run_dir = run_filename.parent.absolute() 182 | filename = run_dir.joinpath(filename) 183 | 184 | if not filename.exists(): 185 | print(_("The file {filename} does not exist.").format(filename=filename)) 186 | return 187 | 188 | session.install(lang=lang, include=include, redirect=redirect) 189 | session.set_formatter(formatter) 190 | 191 | module_globals = editors_helpers.exec_code( 192 | path=filename, lang=lang, include=include 193 | ) 194 | if console: # pragma: no cover 195 | start_console( 196 | local_vars=module_globals, 197 | formatter=formatter, 198 | banner="", 199 | include=include, 200 | background=background, 201 | ipython_prompt=ipython_prompt, 202 | ) 203 | else: 204 | return module_globals 205 | 206 | 207 | def set_formatter( 208 | formatter=None, color_system="auto", force_jupyter=None, background=None 209 | ): 210 | """Sets the default formatter. If no argument is given, a default 211 | formatter is used. 212 | """ 213 | if formatter is not None: 214 | settings.write(option="formatter", value=formatter) 215 | if background is not None: 216 | background = theme.colours.set_background_color(background) 217 | else: 218 | old_background = settings.read(option="background") 219 | if ( 220 | formatter == settings.read(option="formatter") 221 | and old_background is not None 222 | ): 223 | background = old_background 224 | elif background is not None: 225 | background = theme.colours.set_background_color(background) 226 | old_formatter = settings.read(option="formatter") 227 | if old_formatter is None: 228 | return 229 | formatter = old_formatter 230 | old_color_system = settings.read(option="color_system") 231 | if old_color_system is not None: 232 | color_system = old_color_system 233 | old_force_jupyter = settings.read(option="force_jupyter") 234 | if old_force_jupyter is not None: 235 | force_jupyter = old_force_jupyter 236 | 237 | if color_system is not None: 238 | settings.write(option="color_system", value=color_system) 239 | if force_jupyter is not None: 240 | settings.write(option="force_jupyter", value=force_jupyter) 241 | 242 | session.rich_add_vspace = True 243 | session.use_rich = True 244 | session.jupyter_button_style = "" 245 | set_stream() 246 | if isinstance(formatter, str): 247 | settings.write(option="formatter", value=formatter) 248 | if formatter in ["dark", "light"]: 249 | session.console = theme.init_rich_console( 250 | style=formatter, 251 | color_system=color_system, 252 | force_jupyter=force_jupyter, 253 | background=background, 254 | ) 255 | set_stream(redirect=rich_formatters.rich_writer) 256 | formatter = rich_formatters.rich_markdown 257 | elif formatter == "interactive-dark": 258 | session.console = theme.init_rich_console( 259 | style="dark", 260 | color_system=color_system, 261 | force_jupyter=force_jupyter, 262 | background=background, 263 | ) 264 | formatter = rich_formatters.jupyter_interactive 265 | session.jupyter_button_style = ";color:white; background-color:#101010;" 266 | elif formatter in ["interactive", "interactive-light"]: 267 | session.console = theme.init_rich_console( 268 | style="light", 269 | color_system=color_system, 270 | force_jupyter=force_jupyter, 271 | background=background, 272 | ) 273 | formatter = rich_formatters.jupyter_interactive 274 | elif formatter == "jupyter": 275 | formatter = rich_formatters.jupyter 276 | session.use_rich = False 277 | theme.disable_rich() 278 | else: 279 | session.use_rich = False 280 | set_stream() 281 | theme.disable_rich() 282 | if formatter == "plain": 283 | formatter = "repl" 284 | ft_set_formatter(formatter=formatter) 285 | 286 | 287 | def start_console( # pragma: no cover 288 | local_vars=None, 289 | formatter=None, 290 | include="friendly_tb", 291 | lang=None, 292 | banner=None, 293 | background=None, 294 | displayhook=None, 295 | ipython_prompt=True, 296 | ): 297 | """Starts a Friendly console.""" 298 | from . import console 299 | 300 | console.start_console( 301 | local_vars=local_vars, 302 | formatter=formatter, 303 | include=include, 304 | lang=lang, 305 | banner=banner, 306 | background=background, 307 | displayhook=displayhook, 308 | ipython_prompt=ipython_prompt, 309 | ) 310 | 311 | 312 | def friendly_set_lang(lang): 313 | """Sets the language to be used.""" 314 | settings.write(option="lang", value=lang, environment="common") 315 | current_lang.install(lang) 316 | 317 | 318 | add_other_set_lang(friendly_set_lang) 319 | set_lang(get_lang()) 320 | 321 | 322 | def _print_settings(): 323 | """View all saved values""" 324 | settings.print_settings() 325 | 326 | 327 | def print_repl_header(): 328 | """Prints the header at the beginning of an interactive session.""" 329 | _ = current_lang.translate 330 | print(f"friendly_traceback {ft_version}; friendly {__version__}.") 331 | print(_("Type 'Friendly' for basic help.")) 332 | -------------------------------------------------------------------------------- /friendly/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py 3 | --------------- 4 | 5 | Sets up the various options when Friendly is invoked from the 6 | command line. You can find more details by doing:: 7 | 8 | python -m friendly -h 9 | 10 | """ 11 | import argparse 12 | import os 13 | import platform 14 | import runpy 15 | import sys 16 | import warnings 17 | 18 | from importlib import import_module 19 | from pathlib import Path 20 | 21 | from friendly_traceback import explain_traceback, exclude_file_from_traceback, install 22 | from friendly_traceback import __version__ as ft_version 23 | from friendly_traceback import debug_helper 24 | from friendly_traceback.config import session 25 | 26 | from friendly import console, __version__ 27 | from .my_gettext import current_lang 28 | 29 | from friendly.rich_console_helpers import set_formatter 30 | 31 | 32 | versions = "\nfriendly-traceback: {}\nfriendly: {}\nPython: {}\n".format( 33 | ft_version, __version__, platform.python_version() 34 | ) 35 | 36 | 37 | def import_function(dotted_path: str) -> type: 38 | """Import a function from a module, given its dotted path. 39 | 40 | This is a utility function currently used when a custom formatter 41 | is invoked by a command line argument:: 42 | 43 | python -m friendly --formatter custom_formatter 44 | """ 45 | # Used by HackInScience.org 46 | try: 47 | module_path, function_name = dotted_path.rsplit(".", 1) 48 | except ValueError as err: # pragma: no cover 49 | raise ImportError("%s doesn't look like a module path" % dotted_path) from err 50 | 51 | module = import_module(module_path) 52 | 53 | try: 54 | return getattr(module, function_name) 55 | except AttributeError as err: # pragma: no cover 56 | raise ImportError( 57 | 'Module "%s" does not define a "%s" function' % (module_path, function_name) 58 | ) from err 59 | 60 | 61 | parser = argparse.ArgumentParser( 62 | formatter_class=argparse.RawDescriptionHelpFormatter, 63 | description=( 64 | """Friendly makes Python tracebacks easier to understand. 65 | 66 | {versions} 67 | 68 | If no source is given Friendly will start an interactive console. 69 | """.format( 70 | versions=versions 71 | ) 72 | ), 73 | ) 74 | 75 | parser.add_argument( 76 | "source", 77 | nargs="?", 78 | help="""Name of the Python script (path/to/my_program.py) 79 | to be run as though it was the main module, so that its 80 | __name__ does equal '__main__'. 81 | """, 82 | ) 83 | 84 | parser.add_argument( 85 | "args", 86 | nargs="*", 87 | help="""Optional arguments to give to the script specified by source. 88 | """, 89 | ) 90 | 91 | parser.add_argument( 92 | "--lang", 93 | default=current_lang.get_lang(), 94 | help="""This sets the language used by Friendly. 95 | Usually this is a two-letter code such as 'fr' for French. 96 | """, 97 | ) 98 | 99 | parser.add_argument( 100 | "--version", 101 | help="""Only displays the current version. 102 | """, 103 | action="store_true", 104 | ) 105 | 106 | parser.add_argument( 107 | "-f", 108 | "--formatter", 109 | help="""Specifies an output format (bw, dark, light, docs, markdown, or markdown_docs) 110 | or a custom formatter function, as a dotted path. By default, the console 111 | will use dark if it is available. 112 | 113 | For example: --formatter friendly.rich_formatters.markdown is 114 | equivalent to --formatter markdown 115 | """, 116 | ) 117 | 118 | parser.add_argument( 119 | "--background", 120 | help="""Specifies a background color to be used if either the 'dark' or the 'light' 121 | formatter is specified. The color needs to be specified as an hexadecimal 122 | value '#xxxxxx'. The default is black for 'dark' and white for 'light'. 123 | """, 124 | ) 125 | 126 | parser.add_argument("--debug", help="""For developer use.""", action="store_true") 127 | parser.add_argument("--no_debug", help="""For developer use.""", action="store_true") 128 | 129 | parser.add_argument( 130 | "--include", 131 | help="""Specifies what content to include by default in the traceback. 132 | The defaults are 'friendly_tb' if the friendly-console is going to be shown, 133 | otherwise it is 'explain'. 134 | """, 135 | ) 136 | 137 | parser.add_argument( 138 | "--python_prompt", 139 | help="""Specifies that the console prompt must the regular Python prompt""", 140 | action="store_true", 141 | ) 142 | 143 | parser.add_argument( 144 | "-i", help="""Starts the console after executing a source""", action="store_true" 145 | ) 146 | 147 | parser.add_argument( 148 | "-x", 149 | help="""Starts the console after executing a source only 150 | if an exception has been raised""", 151 | action="store_true", 152 | ) 153 | 154 | 155 | def main(): 156 | _ = current_lang.translate 157 | sys.path.insert(0, os.getcwd()) 158 | args = parser.parse_args() 159 | if args.version: # pragma: no cover 160 | print(f"\nFriendly version {__version__}") 161 | if not args.source: 162 | sys.exit() 163 | 164 | include = "friendly_tb" 165 | if args.include: # pragma: no cover 166 | include = args.include 167 | elif args.source and not (sys.flags.interactive or args.i or args.x): 168 | include = "explain" 169 | if args.debug: # pragma: no cover 170 | debug_helper.DEBUG = True 171 | include = "debug_tb" 172 | elif args.no_debug: # pragma: no cover 173 | debug_helper.DEBUG = False 174 | 175 | install(lang=args.lang, include=include) 176 | 177 | if args.formatter: 178 | formatter = args.formatter # noqa 179 | if formatter in ["repl", "dark", "light", "docs"]: 180 | set_formatter(formatter, background=args.background) # pragma: no cover 181 | else: 182 | set_formatter(import_function(args.formatter)) 183 | else: 184 | set_formatter("dark", background=args.background) # pragma: no cover 185 | console_defaults = {} 186 | if args.source is not None: 187 | filename = Path(args.source) 188 | if not filename.exists(): # pragma: no cover 189 | print( 190 | "\n", 191 | _("The file {filename} does not exist.").format(filename=args.source), 192 | ) 193 | return 194 | 195 | exclude_file_from_traceback(runpy.__file__) 196 | sys.argv = [args.source, *args.args] 197 | if sys.flags.interactive or args.i or args.x: 198 | warnings.simplefilter("always") 199 | try: 200 | module_dict = runpy.run_path(args.source, run_name="__main__") 201 | console_defaults.update(module_dict) 202 | except Exception: # noqa 203 | explain_traceback() 204 | if ( 205 | sys.flags.interactive or args.i or (args.x and session.recorded_tracebacks) 206 | ): # pragma: no cover 207 | console.start_console( 208 | local_vars=console_defaults, 209 | background=args.background, 210 | lang=args.lang, 211 | ipython_prompt=not args.python_prompt, 212 | ) 213 | 214 | else: # pragma: no cover 215 | console.start_console( 216 | local_vars=console_defaults, 217 | background=args.background, 218 | lang=args.lang, 219 | ipython_prompt=not args.python_prompt, 220 | ) 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /friendly/console.py: -------------------------------------------------------------------------------- 1 | """ 2 | console.py 3 | ========== 4 | 5 | Adaptation of Python's console found in code.py so that it can be 6 | used to show some "friendly" tracebacks. 7 | """ 8 | import platform 9 | 10 | import friendly_traceback as ft 11 | 12 | from friendly_traceback import ft_console, friendly_exec 13 | from friendly_traceback.config import session 14 | from friendly.rich_console_helpers import helpers 15 | from .my_gettext import current_lang 16 | 17 | import friendly 18 | 19 | 20 | BANNER = "\nfriendly-traceback: {}\nfriendly: {}\nPython: {}\n".format( 21 | ft.__version__, friendly.__version__, platform.python_version() 22 | ) 23 | 24 | _ = current_lang.translate 25 | 26 | 27 | class FriendlyConsole(ft_console.FriendlyTracebackConsole): 28 | # skipcq: PYL-W0622 29 | def __init__( 30 | self, 31 | local_vars=None, 32 | formatter="dark", 33 | background=None, 34 | displayhook=None, 35 | ipython_prompt=True, 36 | ): 37 | """This class builds upon Python's code.InteractiveConsole 38 | to provide friendly tracebacks. It keeps track 39 | of code fragment executed by treating each of them as 40 | an individual source file. 41 | """ 42 | super().__init__( 43 | local_vars=local_vars, 44 | displayhook=displayhook, 45 | ipython_prompt=ipython_prompt, 46 | ) 47 | self.rich_console = False 48 | friendly.set_formatter(formatter, background=background) 49 | if formatter in ["dark", "light"]: 50 | self.rich_console = True 51 | 52 | def raw_input(self, prompt=""): 53 | """Write a prompt and read a line. 54 | The returned line does not include the trailing newline. 55 | When the user enters the EOF key sequence, EOFError is raised. 56 | The base implementation uses the built-in function 57 | input(); a subclass may replace this with a different 58 | implementation. 59 | """ 60 | if self.rich_console: 61 | if "[" in prompt: 62 | prompt = f"\n[operators][[/operators]{self.counter}[operators]]: " 63 | elif "...:" in prompt: 64 | prompt = prompt.replace("...:", "...[operators]:[/operators]") 65 | else: 66 | prompt = "[operators]" + prompt 67 | return session.console.input(prompt) 68 | return input(prompt) 69 | 70 | 71 | def start_console( 72 | local_vars=None, 73 | formatter=None, 74 | include="friendly_tb", 75 | lang=None, 76 | banner=None, 77 | background=None, 78 | displayhook=None, 79 | ipython_prompt=True, 80 | ): 81 | """Starts a console; modified from code.interact""" 82 | if friendly.settings.ENVIRONMENT is None: 83 | if friendly.settings.terminal_type: 84 | friendly.settings.ENVIRONMENT = friendly.settings.terminal_type 85 | else: 86 | friendly.settings.ENVIRONMENT = "terminal" 87 | 88 | if lang is None: 89 | lang = friendly.get_lang() 90 | if banner is None: 91 | banner = BANNER + ft.ft_console.type_friendly() + "\n" 92 | if formatter is None: 93 | formatter = friendly.settings.read(option="formatter") 94 | if formatter is None: 95 | formatter = "dark" 96 | if background is None: 97 | background = friendly.settings.read(option="background") 98 | 99 | ft.set_lang(lang) 100 | 101 | if not ft.is_installed(): 102 | ft.install(include=include, lang=lang) 103 | 104 | if local_vars is not None: 105 | # Make sure we don't overwrite with our own functions 106 | helpers.update(local_vars) 107 | helpers["friendly_exec"] = friendly_exec 108 | helpers["toggle_prompt"] = ft_console.toggle_prompt 109 | 110 | console = FriendlyConsole( 111 | local_vars=helpers, 112 | formatter=formatter, 113 | background=background, 114 | displayhook=displayhook, 115 | ipython_prompt=ipython_prompt, 116 | ) 117 | console.interact(banner=banner) 118 | -------------------------------------------------------------------------------- /friendly/idle/__init__.py: -------------------------------------------------------------------------------- 1 | import friendly 2 | from .main import * # noqa 3 | from .main import install 4 | from friendly import settings 5 | from friendly_traceback.console_helpers import friendly_tb 6 | from friendly_traceback import config, add_ignored_warnings 7 | 8 | settings.ENVIRONMENT = "IDLE" 9 | 10 | 11 | def do_not_show_warnings(warning_instance, warning_type, filename, lineno): 12 | # These warnings occur with friendly_idle 13 | return warning_type == ImportWarning and str(warning_instance) in { 14 | "PatchingFinder.find_spec() not found; falling back to find_module()", 15 | "PatchingLoader.exec_module() not found; falling back to load_module()", 16 | } 17 | 18 | 19 | add_ignored_warnings(do_not_show_warnings) 20 | 21 | __all__ = list(helpers) # noqa 22 | __all__.extend(("run", "start_console", "Friendly")) 23 | __all__.remove("disable") 24 | __all__.remove("enable") 25 | install() 26 | 27 | if config.did_exception_occur_before(): 28 | friendly.print_repl_header() 29 | friendly_tb() # noqa 30 | -------------------------------------------------------------------------------- /friendly/idle/get_syntax.py: -------------------------------------------------------------------------------- 1 | from idlelib import rpc 2 | from friendly.idle import * # noqa 3 | from friendly_traceback import exclude_file_from_traceback 4 | 5 | exclude_file_from_traceback(__file__) 6 | from friendly_traceback.path_info import EXCLUDED_FILE_PATH 7 | 8 | N = -1 9 | entries = {} 10 | 11 | 12 | def get_syntax_error(): 13 | global N 14 | 15 | rpc_handler = rpc.objecttable["exec"].rpchandler 16 | while True: 17 | N += 1 18 | filename = f"" 19 | lines = rpc_handler.remotecall("linecache", "getlines", (filename, None), {}) 20 | if not lines: 21 | N -= 1 22 | break 23 | entries[filename] = "\n".join(lines) 24 | 25 | for i in range(N, -1, -1): 26 | filename = f"" 27 | if filename not in entries: 28 | continue 29 | if entries[filename].replace(" ", "") == "get_syntax_error()": 30 | EXCLUDED_FILE_PATH.add(filename) 31 | continue 32 | compile(entries[filename], filename, "exec") 33 | -------------------------------------------------------------------------------- /friendly/idle/main.py: -------------------------------------------------------------------------------- 1 | """Experimental module to automatically install Friendly 2 | as a replacement for the standard traceback in IDLE.""" 3 | 4 | import inspect 5 | import sys 6 | from pathlib import Path 7 | from functools import partial 8 | 9 | 10 | from idlelib import run as idlelib_run 11 | 12 | import friendly_traceback # noqa 13 | from friendly_traceback.config import session 14 | from friendly_traceback.console_helpers import * # noqa 15 | from friendly_traceback.console_helpers import _nothing_to_show 16 | from friendly_traceback.console_helpers import History, helpers, Friendly # noqa 17 | from friendly_traceback.functions_help import add_help_attribute 18 | 19 | from friendly import get_lang 20 | from friendly import settings 21 | from ..my_gettext import current_lang 22 | from .. import idle_writer 23 | from . import patch_source_cache # noqa 24 | from .get_syntax import get_syntax_error 25 | 26 | 27 | settings.ENVIRONMENT = "IDLE" 28 | friendly_traceback.set_lang(get_lang()) 29 | 30 | friendly_traceback.exclude_file_from_traceback(__file__) 31 | _writer = partial(idle_writer.writer, stream=sys.stdout.shell) 32 | 33 | 34 | class IdleHistory(History): 35 | def __call__(self): 36 | """Prints a list of recorded tracebacks and warning messages""" 37 | if not session.recorded_tracebacks: 38 | info = {"suggest": _nothing_to_show() + "\n"} 39 | explanation = session.formatter(info, include="hint") 40 | session.write_err(explanation) 41 | return 42 | for index, tb in enumerate(session.recorded_tracebacks): 43 | if "message" in tb.info: 44 | info = {"message": f"`{index}.` {tb.info['message']}"} 45 | explanation = session.formatter(info, include="message") 46 | session.write_err(explanation) 47 | 48 | 49 | history = IdleHistory() 50 | add_help_attribute({"history": history}) 51 | Friendly.add_helper(history) 52 | _old_displayhook = sys.displayhook 53 | 54 | helpers["get_syntax_error"] = get_syntax_error 55 | 56 | Friendly.remove_helper("disable") 57 | Friendly.remove_helper("enable") 58 | Friendly.remove_helper("set_formatter") 59 | 60 | 61 | def _displayhook(value): 62 | if value is None: 63 | return 64 | if str(type(value)) == "" and hasattr(value, "__rich_repr__"): 65 | _writer( 66 | [ 67 | (f" {value.__name__}():", "default"), 68 | (f" {value.__rich_repr__()[0]}", "stdout"), 69 | "\n", 70 | ] 71 | ) 72 | return 73 | if hasattr(value, "__friendly_repr__"): 74 | lines = value.__friendly_repr__().split("\n") 75 | for line in lines: 76 | if "`" in line: 77 | newline = [] 78 | parts = line.split("`") 79 | for index, content in enumerate(parts): 80 | if index % 2 == 0: 81 | newline.append((content, "stdout")) 82 | else: 83 | newline.append((content, "default")) 84 | newline.append("\n") 85 | _writer(newline) 86 | elif "():" in line: 87 | parts = line.split("():") 88 | _writer(((f"{ parts[0]}():", "default"), (parts[1], "stdout"), "\n")) 89 | else: 90 | _writer(line + "\n") 91 | return 92 | 93 | _old_displayhook(value) 94 | 95 | 96 | def install_in_idle_shell(lang=get_lang()): 97 | """Installs Friendly in IDLE's shell so that it can retrieve 98 | code entered in IDLE's repl. 99 | Note that this requires Python version 3.10+ since IDLE did not support 100 | custom excepthook in previous versions of Python. 101 | 102 | Furthermore, Friendly is bypassed when code entered in IDLE's repl 103 | raises SyntaxErrors. 104 | """ 105 | friendly_traceback.exclude_file_from_traceback(idlelib_run.__file__) 106 | friendly_traceback.install(include="friendly_tb", redirect=_writer, lang=lang) 107 | 108 | 109 | def install(lang=get_lang()): 110 | """Installs Friendly in the IDLE shell, with a custom formatter. 111 | For Python versions before 3.10, this was not directly supported, so a 112 | Friendly console is used instead of IDLE's shell. 113 | 114 | Changes introduced in Python 3.10 were back-ported to Python 3.9.5 and 115 | to Python 3.8.10. 116 | """ 117 | _ = current_lang.translate 118 | 119 | sys.stderr = sys.stdout.shell # noqa 120 | friendly_traceback.set_formatter(idle_writer.formatter) 121 | if sys.version_info >= (3, 9, 5) or ( 122 | sys.version_info >= (3, 8, 10) and sys.version_info < (3, 9, 0) 123 | ): 124 | install_in_idle_shell(lang=lang) 125 | sys.displayhook = _displayhook 126 | else: 127 | _writer(_("Friendly cannot be installed in this version of IDLE.\n")) 128 | _writer(_("Using Friendly's own console instead.\n")) 129 | start_console(lang=lang, displayhook=_displayhook) 130 | 131 | 132 | def start_console(lang="en", displayhook=None, ipython_prompt=True): 133 | """Starts a Friendly console with a custom formatter for IDLE""" 134 | sys.stderr = sys.stdout.shell # noqa 135 | friendly_traceback.set_stream(_writer) 136 | friendly_traceback.start_console( 137 | formatter=idle_writer.formatter, 138 | lang=lang, 139 | displayhook=displayhook, 140 | ipython_prompt=ipython_prompt, 141 | ) 142 | 143 | 144 | def run( 145 | filename, 146 | lang=get_lang(), 147 | include="friendly_tb", 148 | args=None, 149 | console=True, 150 | ipython_prompt=True, 151 | ): 152 | """This function executes the code found in a Python file. 153 | 154 | ``filename`` should be either an absolute path or, it should be the name of a 155 | file (filename.py) found in the same directory as the file from which ``run()`` 156 | is called. 157 | 158 | If friendly_console is set to ``False`` (the default) and the Python version 159 | is greater or equal to 3.10, ``run()`` returns an empty dict 160 | if a ``SyntaxError`` was raised, otherwise returns the dict in 161 | which the module (``filename``) was executed. 162 | 163 | If console is set to ``True`` (the default), the execution continues 164 | as an interactive session in a Friendly console, with the module 165 | dict being used as the locals dict. 166 | 167 | Other arguments include: 168 | 169 | ``lang``: language used; currently only ``en`` (default) and ``fr`` 170 | are available. 171 | 172 | ``include``: specifies what information is to be included if an 173 | exception is raised. 174 | 175 | ``args``: strings tuple that is passed to the program as though it 176 | was run on the command line as follows:: 177 | 178 | python filename.py arg1 arg2 ... 179 | 180 | 181 | """ 182 | _ = current_lang.translate 183 | 184 | sys.stderr = sys.stdout.shell # noqa 185 | friendly_traceback.set_formatter(idle_writer.formatter) 186 | friendly_traceback.set_stream(_writer) 187 | 188 | filename = Path(filename) 189 | if not filename.is_absolute(): 190 | frame = inspect.stack()[1] 191 | # This is the file from which run() is called 192 | run_filename = Path(frame[0].f_code.co_filename) 193 | run_dir = run_filename.parent.absolute() 194 | filename = run_dir.joinpath(filename) 195 | 196 | if not filename.exists(): 197 | print(_("The file {filename} does not exist.").format(filename=filename)) 198 | return 199 | 200 | if not console: 201 | if sys.version_info >= (3, 9, 5) or ( 202 | sys.version_info >= (3, 8, 10) and sys.version_info < (3, 9, 0) 203 | ): 204 | install_in_idle_shell() 205 | else: 206 | sys.stderr.write("Friendly cannot be installed in this version of IDLE.\n") 207 | 208 | return friendly_traceback.run( 209 | filename, 210 | lang=lang, 211 | include=include, 212 | args=args, 213 | console=console, 214 | formatter=idle_writer.formatter, 215 | ipython_prompt=ipython_prompt, 216 | ) 217 | -------------------------------------------------------------------------------- /friendly/idle/patch_source_cache.py: -------------------------------------------------------------------------------- 1 | import linecache 2 | from idlelib import rpc 3 | 4 | from friendly_traceback import source_cache 5 | 6 | 7 | def _get_lines(filename, linenumber=None): 8 | rpchandler = rpc.objecttable["exec"].rpchandler 9 | lines = rpchandler.remotecall("linecache", "getlines", (filename, None), {}) 10 | new_lines = [] 11 | for line in lines: 12 | if not line.endswith("\n"): 13 | line += "\n" 14 | if filename.startswith("= (3, 9, 5): 14 | repl_indentation["suggest"] = "single" # more appropriate value 15 | 16 | 17 | def writer(output, color=None, stream=None): 18 | """Use this instead of standard sys.stderr to write traceback so that 19 | they can be colorized. 20 | """ 21 | if isinstance(output, str): 22 | if color is None: 23 | stream.write(output, "stderr") # noqa 24 | else: 25 | stream.write(output, color) # noqa 26 | return 27 | for fragment in output: 28 | if isinstance(fragment, str): 29 | stream.write(fragment, "stderr") # noqa 30 | elif len(fragment) == 2: 31 | stream.write(fragment[0], fragment[1]) # noqa 32 | else: 33 | stream.write(fragment[0], "stderr") # noqa 34 | 35 | 36 | # The logic of the formatter is quite convoluted, 37 | # unless one is very familiar with how the basic formatting is done. 38 | # 39 | # All that matters is that, it is debugged and works appropriately! ;-) 40 | # TODO: add unit tests 41 | 42 | 43 | def format_source(text): 44 | """Formats the source code shown by where(). 45 | 46 | Often, the location of an error is indicated by one or more ^ below 47 | the line with the error. IDLE uses highlighting with red background the 48 | normal single character location of an error. 49 | This function replaces the ^ used to highlight an error by the same 50 | highlighting scheme used by IDLE. 51 | """ 52 | lines = text.split("\n") 53 | while not lines[-1].strip(): 54 | lines.pop() 55 | error_lines = get_highlighting_ranges(lines) 56 | 57 | new_lines = [] 58 | for index, line in enumerate(lines): 59 | if index in error_lines: 60 | continue 61 | colon_location = line.find("|") + 1 62 | 63 | if line.lstrip().startswith("-->"): 64 | new_lines.append((line[:colon_location], "stderr")) 65 | else: 66 | new_lines.append((line[:colon_location], "stdout")) 67 | if index + 1 in error_lines: 68 | no_highlight = True 69 | end = -1 70 | for begin, end in error_lines[index + 1]: 71 | text = line[begin:end] 72 | if no_highlight: 73 | if begin < colon_location: 74 | text = line[colon_location:end] 75 | new_lines.append((text, "default")) 76 | no_highlight = False 77 | else: 78 | if not text: 79 | text = " " 80 | new_lines.append((text, "ERROR")) 81 | no_highlight = True 82 | new_lines.append((line[end:], "default")) 83 | else: 84 | new_lines.append((line[colon_location:], "default")) 85 | new_lines.append(("\n", "default")) 86 | return new_lines 87 | 88 | 89 | def format_text(info, item, indentation): 90 | """Format text with embedded code fragment surrounded by back-quote characters.""" 91 | new_lines = [] 92 | text = info[item].rstrip() 93 | for line in text.split("\n"): 94 | if not line.strip(): 95 | continue 96 | if "`" in line and line.count("`") % 2 == 0: 97 | fragments = line.split("`") 98 | for index, fragment in enumerate(fragments): 99 | if index == 0: 100 | new_lines.append((indentation + fragment, "stdout")) 101 | elif index % 2: 102 | if ( 103 | "Error" in fragment 104 | or "Warning" in fragment 105 | or "Exception" in fragment 106 | ) and " " not in fragment.strip(): 107 | new_lines.append((fragment, "stderr")) 108 | else: 109 | new_lines.append((fragment, "default")) 110 | else: 111 | new_lines.append((fragment, "stdout")) 112 | new_lines.append(("\n", "stdout")) 113 | else: 114 | colour = "default" if line.startswith(" ") else "stdout" 115 | new_lines.append((indentation + line + "\n", colour)) 116 | 117 | return new_lines 118 | 119 | 120 | def format_traceback(text): 121 | """We format tracebacks using the default stderr color (usually red) 122 | except that lines with code are shown in the default color (usually black). 123 | """ 124 | lines = text.split("\n") 125 | if lines[-2].startswith("SyntaxError:"): 126 | if lines[2].strip().startswith("File"): 127 | lines = lines[3:] # Remove everything before syntax error 128 | else: 129 | lines = lines[1:] # Remove file name 130 | new_lines = [] 131 | for line in lines: 132 | if line.startswith(" "): 133 | new_lines.append((line, "default")) 134 | elif line: 135 | new_lines.append((line, "stderr")) 136 | new_lines.append(("\n", "default")) 137 | return new_lines 138 | 139 | 140 | def formatter(info, include="friendly_tb"): 141 | """Formatter that takes care of color definitions.""" 142 | items_to_show = select_items(include) 143 | spacing = {"single": " " * 4, "double": " " * 8, "none": ""} 144 | result = ["\n"] 145 | for item in items_to_show: 146 | if item == "header": 147 | continue 148 | 149 | if item in info: 150 | if "traceback" in item: # no additional indentation 151 | result.extend(format_traceback(info[item])) 152 | elif "source" in item: # no additional indentation 153 | result.extend(format_source(info[item])) 154 | elif "header" in item: 155 | indentation = spacing[repl_indentation[item]] 156 | result.append((indentation + info[item], "stderr")) 157 | elif "message" in item: # Highlight error name 158 | parts = info[item].split(":") 159 | parts[0] = "`" + parts[0] + "`" 160 | _info = {item: ":".join(parts)} 161 | indentation = spacing[repl_indentation[item]] 162 | result.extend(format_text(_info, item, indentation)) 163 | elif item == "exception_notes": 164 | result.extend([note + "\n" for note in info[item]]) 165 | else: 166 | indentation = spacing[repl_indentation[item]] 167 | result.extend(format_text(info, item, indentation)) 168 | if "traceback" not in item: 169 | result.extend("\n") 170 | 171 | if result == ["\n"]: 172 | return no_result(info, include) 173 | 174 | if result[-1] == "\n" and include != "friendly_tb": 175 | result.pop() 176 | 177 | return result 178 | -------------------------------------------------------------------------------- /friendly/ipython.py: -------------------------------------------------------------------------------- 1 | """Sets up everything required for an IPython terminal session.""" 2 | from friendly.ipython_common import excepthook 3 | from friendly.ipython_common.settings import init_settings 4 | 5 | from friendly import print_repl_header 6 | from friendly import settings 7 | from friendly_traceback import config 8 | from friendly.rich_console_helpers import * # noqa 9 | from friendly.rich_console_helpers import __all__ # noqa 10 | 11 | import colorama 12 | 13 | colorama.deinit() # Required to get correct colours in Windows terminal 14 | colorama.init(convert=False, strip=False) 15 | 16 | if settings.terminal_type: 17 | settings.ENVIRONMENT = settings.terminal_type + "-ipython" 18 | else: 19 | settings.ENVIRONMENT = "ipython" 20 | 21 | excepthook.enable() 22 | init_settings("dark") 23 | print_repl_header() 24 | if config.did_exception_occur_before(): 25 | friendly_tb() # noqa 26 | -------------------------------------------------------------------------------- /friendly/ipython_common/__init__.py: -------------------------------------------------------------------------------- 1 | from friendly_traceback import add_ignored_warnings 2 | 3 | 4 | def _do_not_show_warning(warning_instance, warning_type, filename, lineno) -> bool: 5 | return filename == "<>" 6 | 7 | 8 | add_ignored_warnings(_do_not_show_warning) 9 | del add_ignored_warnings 10 | -------------------------------------------------------------------------------- /friendly/ipython_common/excepthook.py: -------------------------------------------------------------------------------- 1 | """Automatically installs Friendly as a replacement for the standard traceback in IPython. 2 | 3 | Used in IPython itself and other programming environments based on IPython such as 4 | Jupyter, Google Colab, Mu's repl, etc. 5 | """ 6 | 7 | from friendly_traceback import ( 8 | install, 9 | exclude_file_from_traceback, 10 | explain_traceback, 11 | uninstall, 12 | ) # noqa 13 | 14 | try: 15 | from IPython.core import compilerop, interactiveshell 16 | except ImportError: 17 | raise ValueError("IPython cannot be imported.") 18 | try: 19 | from IPython.utils import py3compat # noqa 20 | except ImportError: 21 | pass 22 | else: 23 | exclude_file_from_traceback(py3compat.__file__) 24 | 25 | exclude_file_from_traceback(interactiveshell.__file__) 26 | exclude_file_from_traceback(compilerop.__file__) 27 | 28 | 29 | original_exception_hooks = {} 30 | 31 | 32 | def enable(): 33 | """Installs friendly-traceback as an exception hook in IPython""" 34 | # save old values 35 | original_exception_hooks[ 36 | "showtraceback" 37 | ] = interactiveshell.InteractiveShell.showtraceback 38 | original_exception_hooks[ 39 | "showsyntaxerror" 40 | ] = interactiveshell.InteractiveShell.showsyntaxerror 41 | interactiveshell.InteractiveShell.showtraceback = ( 42 | lambda self, *args, **kwargs: explain_traceback() 43 | ) 44 | interactiveshell.InteractiveShell.showsyntaxerror = ( 45 | lambda self, *args, **kwargs: explain_traceback() 46 | ) 47 | install(include="friendly_tb") 48 | 49 | 50 | def disable(): 51 | if not original_exception_hooks: 52 | print("friendly is not installed.") 53 | return 54 | uninstall() 55 | interactiveshell.InteractiveShell.showtraceback = original_exception_hooks[ 56 | "showtraceback" 57 | ] 58 | interactiveshell.InteractiveShell.showsyntaxerror = original_exception_hooks[ 59 | "showsyntaxerror" 60 | ] 61 | original_exception_hooks.clear() 62 | -------------------------------------------------------------------------------- /friendly/ipython_common/settings.py: -------------------------------------------------------------------------------- 1 | from friendly import settings 2 | from friendly_traceback import config 3 | from friendly.rich_console_helpers import set_formatter 4 | 5 | 6 | def init_settings(default_formatter="dark"): 7 | """Initialises the formatter using saved settings for the 8 | current environment. 9 | """ 10 | if settings.has_environment(settings.ENVIRONMENT): 11 | formatter = settings.read(option="formatter") 12 | background = settings.read(option="background") 13 | color_system = settings.read(option="color_system") 14 | force_jupyter = settings.read(option="force_jupyter") 15 | set_formatter( 16 | formatter=formatter, 17 | color_system=color_system, 18 | force_jupyter=force_jupyter, 19 | background=background, 20 | ) 21 | else: 22 | set_formatter(default_formatter) 23 | 24 | if settings.has_environment(settings.ENVIRONMENT): 25 | _ipython_prompt = settings.read(option="ipython_prompt") 26 | if _ipython_prompt in [None, "True"]: 27 | config.session.ipython_prompt = True 28 | else: 29 | config.session.ipython_prompt = False 30 | else: 31 | config.session.ipython_prompt = True 32 | settings.write(option="ipython_prompt", value="True") 33 | -------------------------------------------------------------------------------- /friendly/jupyter.py: -------------------------------------------------------------------------------- 1 | from rich import jupyter as rich_jupyter 2 | 3 | """Sets up everything required for an IPython terminal session.""" 4 | from friendly.ipython_common import excepthook 5 | from friendly.ipython_common.settings import init_settings 6 | 7 | from friendly import print_repl_header 8 | from friendly import settings 9 | from friendly_traceback import config 10 | from friendly.rich_console_helpers import * # noqa 11 | 12 | from friendly_traceback import session # noqa 13 | from friendly_traceback.functions_help import ( 14 | add_help_attribute, 15 | short_description, 16 | ) # noqa 17 | 18 | # from .ipython import * # noqa 19 | from friendly.my_gettext import current_lang # noqa 20 | from friendly.rich_console_helpers import * # noqa 21 | from friendly.rich_console_helpers import helpers, Friendly 22 | 23 | _ = current_lang.translate 24 | 25 | 26 | if settings.terminal_type: 27 | settings.ENVIRONMENT = settings.terminal_type + "-jupyter" 28 | else: 29 | settings.ENVIRONMENT = "jupyter" 30 | 31 | 32 | # For Jupyter output, Rich specifies a set of fonts starting with Menlo and 33 | # ending with monospace as last resort whereas Jupyter notebooks just 34 | # specify monospace. To make font-size more consistent, we remove the 35 | # font-specification from Rich. 36 | rich_jupyter.JUPYTER_HTML_FORMAT = ( 37 | "
{code}
" 38 | ) 39 | old_light = light # noqa 40 | old_dark = dark # noqa 41 | 42 | 43 | def light(): 44 | set_formatter("interactive") # noqa 45 | 46 | 47 | def dark(): 48 | set_formatter("interactive-dark") # noqa 49 | 50 | 51 | light.__doc__ = old_light.__doc__ 52 | Friendly.light = light # noqa 53 | helpers["light"] = light 54 | 55 | dark.__doc__ = old_dark.__doc__ 56 | Friendly.dark = dark # noqa 57 | helpers["dark"] = dark 58 | 59 | 60 | def set_tb_width(width=None): 61 | """Sets the width of the traceback when using a rich-based 62 | formatter in a Jupyter notebook or equivalent. 63 | 64 | The width of traceback is never less than the width of 65 | the other output from rich. 66 | """ 67 | if width is None: 68 | return 69 | try: 70 | session.console.width = width 71 | except Exception: # noqa 72 | return 73 | session.rich_tb_width = width 74 | if session.rich_width is None or session.rich_width > session.rich_tb_width: 75 | session.rich_width = width 76 | 77 | 78 | short_description["set_tb_width"] = lambda: _("Sets the width of the traceback.") 79 | add_help_attribute({"set_tb_width": set_tb_width}) 80 | 81 | Friendly.add_helper(set_tb_width) 82 | helpers["set_tb_width"] = set_tb_width 83 | 84 | __all__ = list(helpers.keys()) 85 | 86 | excepthook.enable() 87 | # Use the new interactive light formatter by default. 88 | init_settings("interactive-light") 89 | set_tb_width(100) # noqa 90 | set_width(70) # noqa 91 | session.is_jupyter = True 92 | print_repl_header() 93 | if config.did_exception_occur_before(): 94 | friendly_tb() # noqa 95 | -------------------------------------------------------------------------------- /friendly/locales/es/LC_MESSAGES/friendly_es.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/friendly/locales/es/LC_MESSAGES/friendly_es.mo -------------------------------------------------------------------------------- /friendly/locales/es/LC_MESSAGES/friendly_es.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: friendly\n" 4 | "POT-Creation-Date: 2022-01-26 05:36-0400\n" 5 | "PO-Revision-Date: \n" 6 | "Last-Translator: André Roberge \n" 7 | "Language-Team: \n" 8 | "Language: es_AR\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.0.1\n" 13 | 14 | #: __init__.py:177 __main__.py:175 idle\main.py:220 15 | msgid "The file {filename} does not exist." 16 | msgstr "El archivo {filename} no existe." 17 | 18 | #: __init__.py:324 19 | msgid "Type 'Friendly' for basic help." 20 | msgstr "" 21 | "Escriba 'Friendly' para obtener ayuda sobre funciones/métodos especiales." 22 | 23 | #: console.py:28 24 | msgid "" 25 | " Do you find these warnings useful?\n" 26 | " Comment at https://github.com/friendly-traceback/friendly-traceback/" 27 | "issues/7.\n" 28 | "\n" 29 | " You can use www('warnings') to go to that url." 30 | msgstr "" 31 | 32 | #: console.py:95 33 | msgid "Warning: you added a type hint to the python builtin `{name}`." 34 | msgstr "" 35 | "Advertencia: agregaste un indicador de tipo al objeto incorporado de python " 36 | "`{name}`." 37 | 38 | #: console.py:98 39 | msgid "" 40 | "Warning: you used a type hint for a variable without assigning it a value.\n" 41 | msgstr "" 42 | "Advertencia: usaste un indicador de tipo para una variable sin asignarle un " 43 | "valor.\n" 44 | 45 | #: console.py:101 46 | msgid "Instead of `{hint}`, perhaps you meant `{assignment}`." 47 | msgstr "En lugar de `{hint}`, quizás quisiste decir `{assignment}`." 48 | 49 | #: console.py:171 50 | msgid "Warning: you have redefined the python builtin `{name}`." 51 | msgstr "Advertencia: redefiniste el objeto incorporado de python `{name}`." 52 | 53 | #: idle\main.py:150 54 | msgid "Friendly cannot be installed in this version of IDLE.\n" 55 | msgstr "" 56 | 57 | #: idle\main.py:151 58 | msgid "Using Friendly's own console instead.\n" 59 | msgstr "" 60 | 61 | #: jupyter.py:78 62 | msgid "Sets the width of the traceback." 63 | msgstr "Establece la anchura del rastreo." 64 | 65 | #: mu\repl.py:64 66 | msgid "set_width() is only available using 'day', 'night' or 'colourful' mode." 67 | msgstr "" 68 | "set_width() sólo está disponible en modo \"day\", \"night\" o \"colourful\"." 69 | 70 | #: mu\repl.py:104 71 | msgid "Colour scheme designed for Mu's day theme." 72 | msgstr "Combinación de colores diseñada para el tema 'día' de Mu." 73 | 74 | #: mu\repl.py:105 75 | msgid "Colour scheme designed for Mu's night theme." 76 | msgstr "Combinación de colores diseñada para el tema 'noche' de Mu." 77 | 78 | #: mu\repl.py:106 79 | msgid "" 80 | "Colourful scheme with black background suitable for Mu's high contrast theme." 81 | msgstr "" 82 | "Esquema colorido con fondo negro adecuado para el tema de alto contraste de " 83 | "Mu." 84 | 85 | #: mu\repl.py:109 86 | msgid "White text on black background; suitable for Mu's high contrast theme." 87 | msgstr "" 88 | "Texto blanco sobre fondo negro; adecuado para el tema de alto contraste de " 89 | "Mu." 90 | 91 | #: mu\runner.py:16 92 | msgid "Friendly themes are only available in Mu's REPL.\n" 93 | msgstr "Los temas amigables sólo están disponibles en el REPL de Mu.\n" 94 | 95 | #: rich_console_helpers.py:86 96 | msgid "Sets a colour scheme designed for a black background." 97 | msgstr "Establece un esquema de color diseñado para un fondo negro." 98 | 99 | #: rich_console_helpers.py:89 100 | msgid "Sets a colour scheme designed for a white background." 101 | msgstr "Establece una combinación de colores diseñada para un fondo blanco." 102 | 103 | #: rich_console_helpers.py:92 104 | msgid "Plain formatting, with no colours added." 105 | msgstr "Formato simple, sin colores añadidos." 106 | 107 | #: rich_console_helpers.py:93 108 | msgid "Sets the background color." 109 | msgstr "Establece el color de fondo." 110 | 111 | #: rich_console_helpers.py:94 112 | msgid "Sets the output width in some modes." 113 | msgstr "Establece el ancho de la salida en algunos modos." 114 | 115 | #: rich_console_helpers.py:95 116 | msgid "Prints the saved settings." 117 | msgstr "" 118 | 119 | #: rich_formatters.py:128 120 | msgid "Hide" 121 | msgstr "Ocultar" 122 | 123 | #: rich_formatters.py:191 124 | msgid "More ..." 125 | msgstr "Más ..." 126 | 127 | #: rich_formatters.py:192 128 | msgid "Show message only" 129 | msgstr "Mostrar sólo el mensaje" 130 | 131 | #: theme\__init__.py:29 132 | msgid "" 133 | "Invalid color {color}.\n" 134 | "Colors must be of the form #dddddd." 135 | msgstr "" 136 | "Color {color} no válido.\n" 137 | "Los colores deben ser en la forma #dddddd." 138 | 139 | #~ msgid "No SyntaxError recorded." 140 | #~ msgstr "No se ha registrado ningún SyntaxError." 141 | 142 | #~ msgid "Not yet implemented." 143 | #~ msgstr "Aún no implementado." 144 | 145 | #~ msgid "set_width() has no effect with this formatter." 146 | #~ msgstr "set_width() no tiene efecto en este formateador." 147 | 148 | #~ msgid "set_formatter is not supported by ipython_plain." 149 | #~ msgstr "set_formatter no es soportado por ipython_plain." 150 | 151 | #~ msgid "" 152 | #~ "Please report this example to https://github.com/aroberge/friendly/" 153 | #~ "issues.\n" 154 | #~ "If you are using a REPL, use `www('bug')` to do so.\n" 155 | #~ "\n" 156 | #~ msgstr "" 157 | #~ "Por favor, informe de este ejemplo a https://github.com/aroberge/friendly/" 158 | #~ "issues.\n" 159 | #~ "Si está usando un REPL, use `www('bug')` para hacerlo.\n" 160 | 161 | #~ msgid "No information is known about this exception.\n" 162 | #~ msgstr "No se conoce ninguna información sobre esta excepción.\n" 163 | 164 | #~ msgid "" 165 | #~ "If you are using the Friendly console, use `www()` to\n" 166 | #~ "do an Internet search for this particular case.\n" 167 | #~ msgstr "" 168 | #~ "Si está utilizando la consola Friendly, utilice `www()` para\n" 169 | #~ "para hacer una búsqueda en Internet de este caso en particular.\n" 170 | 171 | #~ msgid "Internal error for Friendly.\n" 172 | #~ msgstr "Error interno para Friendly.\n" 173 | -------------------------------------------------------------------------------- /friendly/locales/fr/LC_MESSAGES/friendly_fr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/friendly/locales/fr/LC_MESSAGES/friendly_fr.mo -------------------------------------------------------------------------------- /friendly/locales/friendly.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR ORGANIZATION 3 | # FIRST AUTHOR , YEAR. 4 | # 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: PACKAGE VERSION\n" 8 | "POT-Creation-Date: 2022-01-26 05:36-0400\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Last-Translator: FULL NAME \n" 11 | "Language-Team: LANGUAGE \n" 12 | "MIME-Version: 1.0\n" 13 | "Content-Type: text/plain; charset=cp1252\n" 14 | "Content-Transfer-Encoding: 8bit\n" 15 | "Generated-By: pygettext.py 1.5\n" 16 | 17 | 18 | #: __init__.py:177 __main__.py:175 idle\main.py:220 19 | msgid "The file {filename} does not exist." 20 | msgstr "" 21 | 22 | #: __init__.py:324 23 | msgid "Type 'Friendly' for basic help." 24 | msgstr "" 25 | 26 | #: console.py:28 27 | msgid "" 28 | " Do you find these warnings useful?\n" 29 | " Comment at https://github.com/friendly-traceback/friendly-traceback/issues/7.\n" 30 | "\n" 31 | " You can use www('warnings') to go to that url." 32 | msgstr "" 33 | 34 | #: console.py:95 35 | msgid "Warning: you added a type hint to the python builtin `{name}`." 36 | msgstr "" 37 | 38 | #: console.py:98 39 | msgid "" 40 | "Warning: you used a type hint for a variable without assigning it a value.\n" 41 | msgstr "" 42 | 43 | #: console.py:101 44 | msgid "Instead of `{hint}`, perhaps you meant `{assignment}`." 45 | msgstr "" 46 | 47 | #: console.py:171 48 | msgid "Warning: you have redefined the python builtin `{name}`." 49 | msgstr "" 50 | 51 | #: idle\main.py:150 52 | msgid "" 53 | "Friendly cannot be installed in this version of IDLE.\n" 54 | msgstr "" 55 | 56 | #: idle\main.py:151 57 | msgid "" 58 | "Using Friendly's own console instead.\n" 59 | msgstr "" 60 | 61 | #: jupyter.py:78 62 | msgid "Sets the width of the traceback." 63 | msgstr "" 64 | 65 | #: mu\repl.py:64 66 | msgid "set_width() is only available using 'day', 'night' or 'colourful' mode." 67 | msgstr "" 68 | 69 | #: mu\repl.py:104 70 | msgid "Colour scheme designed for Mu's day theme." 71 | msgstr "" 72 | 73 | #: mu\repl.py:105 74 | msgid "Colour scheme designed for Mu's night theme." 75 | msgstr "" 76 | 77 | #: mu\repl.py:106 78 | msgid "Colourful scheme with black background suitable for Mu's high contrast theme." 79 | msgstr "" 80 | 81 | #: mu\repl.py:109 82 | msgid "White text on black background; suitable for Mu's high contrast theme." 83 | msgstr "" 84 | 85 | #: mu\runner.py:16 86 | msgid "" 87 | "Friendly themes are only available in Mu's REPL.\n" 88 | msgstr "" 89 | 90 | #: rich_console_helpers.py:86 91 | msgid "Sets a colour scheme designed for a black background." 92 | msgstr "" 93 | 94 | #: rich_console_helpers.py:89 95 | msgid "Sets a colour scheme designed for a white background." 96 | msgstr "" 97 | 98 | #: rich_console_helpers.py:92 99 | msgid "Plain formatting, with no colours added." 100 | msgstr "" 101 | 102 | #: rich_console_helpers.py:93 103 | msgid "Sets the background color." 104 | msgstr "" 105 | 106 | #: rich_console_helpers.py:94 107 | msgid "Sets the output width in some modes." 108 | msgstr "" 109 | 110 | #: rich_console_helpers.py:95 111 | msgid "Prints the saved settings." 112 | msgstr "" 113 | 114 | #: rich_formatters.py:128 115 | msgid "Hide" 116 | msgstr "" 117 | 118 | #: rich_formatters.py:191 119 | msgid "More ..." 120 | msgstr "" 121 | 122 | #: rich_formatters.py:192 123 | msgid "Show message only" 124 | msgstr "" 125 | 126 | #: theme\__init__.py:29 127 | msgid "" 128 | "Invalid color {color}.\n" 129 | "Colors must be of the form #dddddd." 130 | msgstr "" 131 | 132 | -------------------------------------------------------------------------------- /friendly/locales/ru/LC_MESSAGES/friendly_ru.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/friendly/locales/ru/LC_MESSAGES/friendly_ru.mo -------------------------------------------------------------------------------- /friendly/locales/ru/LC_MESSAGES/friendly_ru.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: friendly\n" 4 | "POT-Creation-Date: 2022-01-26 05:36-0400\n" 5 | "PO-Revision-Date: 2022-09-18 15:00-0300\n" 6 | "Last-Translator: \n" 7 | "Language-Team: \n" 8 | "Language: ru\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "X-Generator: Poedit 3.1.1\n" 13 | 14 | #: __init__.py:177 __main__.py:175 idle\main.py:220 15 | msgid "The file {filename} does not exist." 16 | msgstr "Файл {filename} не найден." 17 | 18 | #: __init__.py:324 19 | msgid "Type 'Friendly' for basic help." 20 | msgstr "Напечатайте 'Friendly' для получения базовой помощи." 21 | 22 | #: console.py:28 23 | msgid "" 24 | " Do you find these warnings useful?\n" 25 | " Comment at https://github.com/friendly-traceback/friendly-traceback/" 26 | "issues/7.\n" 27 | "\n" 28 | " You can use www('warnings') to go to that url." 29 | msgstr "" 30 | " Вы считаете эти предупреждения полезными?\n" 31 | " Оставляйте комментарии на https://github.com/friendly-traceback/friendly-" 32 | "traceback/issues/7.\n" 33 | "\n" 34 | " Вы можете использовать www('warnings') для перехода по этому адресу." 35 | 36 | #: console.py:95 37 | msgid "Warning: you added a type hint to the python builtin `{name}`." 38 | msgstr "" 39 | "Warning: вы добавили подсказку типа для встроенного модуля Python `{name}`." 40 | 41 | #: console.py:98 42 | msgid "" 43 | "Warning: you used a type hint for a variable without assigning it a value.\n" 44 | msgstr "" 45 | "Warning: вы использовали подсказку типа для переменной, не присвоив той " 46 | "значения.\n" 47 | 48 | #: console.py:101 49 | msgid "Instead of `{hint}`, perhaps you meant `{assignment}`." 50 | msgstr "Вместо `{hint}` вы, возможно, имели в виду `{assignment}`." 51 | 52 | #: console.py:171 53 | msgid "Warning: you have redefined the python builtin `{name}`." 54 | msgstr "Warning: вы переопределили встроенный модуль Python `{name}`." 55 | 56 | #: idle\main.py:150 57 | msgid "Friendly cannot be installed in this version of IDLE.\n" 58 | msgstr "Friendly не может быть установлена в этой версии IDLE.\n" 59 | 60 | #: idle\main.py:151 61 | msgid "Using Friendly's own console instead.\n" 62 | msgstr "Вместо этого используется собственная консоль Friendly.\n" 63 | 64 | #: jupyter.py:78 65 | msgid "Sets the width of the traceback." 66 | msgstr "Задает ширину стека трассировки." 67 | 68 | #: mu\repl.py:64 69 | msgid "set_width() is only available using 'day', 'night' or 'colourful' mode." 70 | msgstr "set_width() доступна только в режимах 'day', 'night', 'colourful'." 71 | 72 | #: mu\repl.py:104 73 | msgid "Colour scheme designed for Mu's day theme." 74 | msgstr "Цветовая схема для дневной темы Mu." 75 | 76 | #: mu\repl.py:105 77 | msgid "Colour scheme designed for Mu's night theme." 78 | msgstr "Цветовая схема для ночной темы Mu." 79 | 80 | #: mu\repl.py:106 81 | msgid "" 82 | "Colourful scheme with black background suitable for Mu's high contrast theme." 83 | msgstr "Цветовая схема с черным фоном подходит для высококонтрастной темы Mu." 84 | 85 | #: mu\repl.py:109 86 | msgid "White text on black background; suitable for Mu's high contrast theme." 87 | msgstr "Белый текст на черном фоне; подходит для высококонтрастной темы Mu." 88 | 89 | #: mu\runner.py:16 90 | msgid "Friendly themes are only available in Mu's REPL.\n" 91 | msgstr "Friendly themes доступны только в REPL Mu.\n" 92 | 93 | #: rich_console_helpers.py:86 94 | msgid "Sets a colour scheme designed for a black background." 95 | msgstr "Устанавливает цветовую схему, предназначенную для черного фона." 96 | 97 | #: rich_console_helpers.py:89 98 | msgid "Sets a colour scheme designed for a white background." 99 | msgstr "Устанавливает цветовую схему, предназначенную для белого фона." 100 | 101 | #: rich_console_helpers.py:92 102 | msgid "Plain formatting, with no colours added." 103 | msgstr "Обычное форматирование, без раскраски." 104 | 105 | #: rich_console_helpers.py:93 106 | msgid "Sets the background color." 107 | msgstr "Устанавливает цвет фона." 108 | 109 | #: rich_console_helpers.py:94 110 | msgid "Sets the output width in some modes." 111 | msgstr "Устанавливает ширину вывода в некоторых режимах." 112 | 113 | #: rich_console_helpers.py:95 114 | msgid "Prints the saved settings." 115 | msgstr "Выводит сохранённые настройки." 116 | 117 | #: rich_formatters.py:128 118 | msgid "Hide" 119 | msgstr "Скрыть" 120 | 121 | #: rich_formatters.py:191 122 | msgid "More ..." 123 | msgstr "Больше ..." 124 | 125 | #: rich_formatters.py:192 126 | msgid "Show message only" 127 | msgstr "Показать только сообщение" 128 | 129 | #: theme\__init__.py:29 130 | msgid "" 131 | "Invalid color {color}.\n" 132 | "Colors must be of the form #dddddd." 133 | msgstr "" 134 | "Некорректный цвет {color}.\n" 135 | "Цвет должен быть следующего формата: #dddddd." 136 | -------------------------------------------------------------------------------- /friendly/make_pot.bat: -------------------------------------------------------------------------------- 1 | py c:\users\andre\appdata\local\programs\python\python310\tools\i18n\pygettext.py -v -p locales -d friendly *.py */*.py 2 | -------------------------------------------------------------------------------- /friendly/mu/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import sys 3 | 4 | if "InteractiveShell" in repr(sys.excepthook): 5 | from .repl import * 6 | from .repl import __all__ 7 | 8 | else: 9 | from .runner import * 10 | from .runner import __all__ 11 | -------------------------------------------------------------------------------- /friendly/mu/repl.py: -------------------------------------------------------------------------------- 1 | """Mu's repl uses an IPython QtConsole. 2 | Mu has three themes designated (in English) as 'day', 'night', 3 | and 'high contrast'.""" 4 | 5 | import json 6 | import os 7 | 8 | import platformdirs 9 | import colorama # noqa 10 | from friendly_traceback import set_stream 11 | from friendly_traceback import set_formatter as ft_set_formatter 12 | from friendly_traceback.config import session, did_exception_occur_before # noqa 13 | from friendly_traceback.functions_help import add_help_attribute, short_description 14 | 15 | from ..my_gettext import current_lang # noqa 16 | from friendly.ipython_common import excepthook 17 | from friendly.rich_console_helpers import * # noqa 18 | from friendly.rich_console_helpers import Friendly, helpers 19 | from friendly import print_repl_header 20 | from friendly import settings 21 | from friendly import theme 22 | from friendly import rich_formatters 23 | 24 | colorama.deinit() # reset needed on Windows 25 | colorama.init(convert=False, strip=False) 26 | _ = current_lang.translate 27 | 28 | settings.ENVIRONMENT = "mu" 29 | mu_data_dir = platformdirs.user_data_dir(appname="mu", appauthor="python") 30 | 31 | 32 | def set_formatter(formatter=None, background=None): 33 | """Sets the default formatter. If no argument is given, a default 34 | formatter is used. 35 | """ 36 | settings.write(option="formatter", value=formatter) 37 | if background is not None: 38 | settings.write(option="background", value=background) 39 | if formatter in ["colourful", "day", "night"]: 40 | style = "light" if formatter == "day" else "dark" 41 | session.console = theme.init_rich_console( 42 | style=style, 43 | color_system="truecolor", 44 | force_jupyter=False, 45 | background=background, 46 | ) 47 | session.use_rich = True 48 | session.rich_add_vspace = True 49 | formatter = rich_formatters.rich_markdown 50 | set_stream(redirect=rich_formatters.rich_writer) 51 | else: 52 | session.use_rich = False 53 | session.rich_add_vspace = False 54 | set_stream() 55 | ft_set_formatter(formatter=formatter) 56 | 57 | 58 | def set_width(width=80): 59 | """Sets the width in a iPython/Jupyter session using 'light' or 'dark' mode""" 60 | if session.use_rich: 61 | session.console._width = width 62 | else: 63 | print( 64 | _("set_width() is only available using 'day', 'night' or 'colourful' mode.") 65 | ) 66 | 67 | 68 | add_help_attribute({"set_formatter": set_formatter, "set_width": set_width}) 69 | Friendly.add_helper(set_formatter) 70 | Friendly.add_helper(set_width) 71 | helpers["set_formatter"] = set_formatter 72 | helpers["set_width"] = set_width 73 | 74 | 75 | # ========= Replacing theme-based formatters 76 | del helpers["dark"] 77 | del helpers["light"] 78 | del helpers["plain"] 79 | Friendly.remove_helper("dark") 80 | Friendly.remove_helper("light") 81 | Friendly.remove_helper("plain") 82 | 83 | 84 | def day(): 85 | """Day theme for Mu's REPL""" 86 | set_formatter("day", background="#FEFEF7") 87 | 88 | 89 | def night(): 90 | """Night theme for Mu's REPL""" 91 | set_formatter("night", background="#373737") 92 | 93 | 94 | def colourful(): 95 | """Colourful theme with black background.""" 96 | set_formatter("colourful", background="#000000") 97 | 98 | 99 | def contrast(): 100 | """White text on black background.""" 101 | set_formatter("repl", background="#000000") 102 | 103 | 104 | short_description["day"] = lambda: _("Colour scheme designed for Mu's day theme.") 105 | short_description["night"] = lambda: _("Colour scheme designed for Mu's night theme.") 106 | short_description["colourful"] = lambda: _( 107 | "Colourful scheme with black background suitable for Mu's high contrast theme." 108 | ) 109 | short_description["contrast"] = lambda: _( 110 | "White text on black background; suitable for Mu's high contrast theme." 111 | ) 112 | local_helpers = { 113 | "day": day, 114 | "night": night, 115 | "colourful": colourful, 116 | "contrast": contrast, 117 | } 118 | add_help_attribute(local_helpers) 119 | 120 | for helper in local_helpers: 121 | Friendly.add_helper(local_helpers[helper]) 122 | helpers.update(local_helpers) 123 | __all__ = list(helpers.keys()) 124 | 125 | 126 | excepthook.enable() 127 | 128 | try: 129 | with open(os.path.join(mu_data_dir, "session.json")) as fp: 130 | mu_settings = json.load(fp) 131 | except FileNotFoundError: 132 | mu_settings = {} 133 | 134 | if "theme" in mu_settings: 135 | mu_theme = mu_settings["theme"] 136 | if mu_theme == "day": 137 | day() 138 | elif mu_theme == "night": 139 | night() 140 | elif settings.has_environment("mu"): 141 | formatter = settings.read(option="formatter") 142 | background = settings.read(option="background") 143 | if formatter == "dark" and background == "#000000": 144 | colourful() 145 | else: 146 | contrast() 147 | else: 148 | contrast() 149 | else: 150 | day() 151 | if "locale" in mu_settings: 152 | set_lang(mu_settings["locale"]) # noqa 153 | elif settings.has_environment("common"): 154 | lang = settings.read(option="lang") 155 | if lang is not None: 156 | set_lang(lang) # noqa 157 | 158 | print_repl_header() 159 | if did_exception_occur_before(): 160 | friendly_tb() # noqa 161 | -------------------------------------------------------------------------------- /friendly/mu/runner.py: -------------------------------------------------------------------------------- 1 | """Mu's runner launches a Python interpreter and feeds back the result 2 | into a console that does not support colour. 3 | """ 4 | import sys # noqa 5 | from ..my_gettext import current_lang # noqa 6 | 7 | from friendly_traceback.runtime_errors import name_error 8 | from friendly_traceback import run, start_console, install # noqa 9 | 10 | from friendly_traceback.console_helpers import * # noqa 11 | from friendly_traceback.console_helpers import __all__ 12 | 13 | 14 | def _cause(): 15 | _ = current_lang.translate 16 | return _("Friendly themes are only available in Mu's REPL.\n") 17 | 18 | 19 | for name in ("bw", "day", "night", "black"): 20 | name_error.CUSTOM_NAMES[name] = _cause 21 | 22 | __all__.append("run") 23 | __all__.append("start_console") 24 | install(redirect=sys.stderr.write, include="friendly_tb") 25 | -------------------------------------------------------------------------------- /friendly/my_gettext.py: -------------------------------------------------------------------------------- 1 | """my_gettext.py 2 | 3 | The usual pattern when using gettext is to surround strings to be translated 4 | by a call to a function named _, as in 5 | 6 | _("This string should be translated.") 7 | 8 | This is done when having gettext "install" a given language: it adds a function 9 | named _ to the global builtins. However, this can fail if some other program, 10 | such as the Python REPL, also modifies the builtins and define _ in its own 11 | ways. 12 | 13 | To avoid such problems, we use a custom class that keep track of the language 14 | preferred for the translation. Inside any namespace, when we need to 15 | provide a translation, we define locally _ to be 16 | 17 | _ = current_lang.translate 18 | 19 | where current_lang.translate means gettext.translation().gettext where 20 | gettext.translation() is the class-based API for gettext. 21 | """ 22 | 23 | import gettext 24 | import os 25 | 26 | from . import settings 27 | from friendly_traceback import debug_helper 28 | 29 | 30 | class LangState: 31 | def __init__(self): 32 | self._translate = lambda text: text 33 | self.lang = self.get_lang() 34 | 35 | def get_lang(self): 36 | """Gets the current saved language""" 37 | return settings.get_lang() 38 | 39 | def install(self, lang=None): 40 | """Sets the language to be used for translations""" 41 | if lang is None: 42 | lang = self.get_lang() 43 | try: 44 | # We first look for the exact language requested. 45 | _lang = gettext.translation( 46 | "friendly_" + lang, 47 | localedir=os.path.normpath( 48 | os.path.join(os.path.dirname(__file__), "locales") 49 | ), 50 | languages=[lang], 51 | fallback=False, 52 | ) 53 | except FileNotFoundError: 54 | # If it is not available, we make it possible to replace a 55 | # language specific to a region, as in fr_CA, by a more 56 | # generic version, such as fr, defined by a two-letter code. 57 | lang = lang[:2] 58 | _lang = gettext.translation( 59 | "friendly_" + lang, 60 | localedir=os.path.normpath( 61 | os.path.join(os.path.dirname(__file__), "locales") 62 | ), 63 | languages=[lang], 64 | fallback=True, # This means that the hard-coded strings in 65 | # the source file will be used if the requested language 66 | # is not available. 67 | ) 68 | self.lang = lang 69 | self._translate = _lang.gettext 70 | 71 | def translate(self, text): 72 | translation = self._translate(text) 73 | if translation == text and self.lang == "fr": # pragma: no cover 74 | debug_helper.log(f"Potentially untranslated text for {self.lang}:") 75 | debug_helper.log(text) 76 | return translation 77 | 78 | 79 | current_lang = LangState() # noqa 80 | -------------------------------------------------------------------------------- /friendly/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The ``friendly`` package uses inline types. 2 | -------------------------------------------------------------------------------- /friendly/rich_console_helpers.py: -------------------------------------------------------------------------------- 1 | """In this module, we modify the basic console helpers for friendly-traceback 2 | to add custom ones for Rich-based formatters.""" 3 | 4 | from friendly_traceback.console_helpers import * # noqa; include Friendly below 5 | from friendly_traceback.console_helpers import Friendly, helpers 6 | from friendly_traceback.functions_help import add_help_attribute, short_description 7 | from friendly_traceback.config import session 8 | 9 | from friendly.my_gettext import current_lang 10 | from friendly import _print_settings 11 | from friendly.settings import _remove_environment 12 | from friendly.theme import colours 13 | 14 | # The following is different from the one imported via the import * above 15 | from friendly import set_formatter 16 | 17 | helpers["set_formatter"] = set_formatter 18 | add_help_attribute({"set_formatter": set_formatter}) 19 | Friendly.add_helper(set_formatter) 20 | 21 | _ = current_lang.translate 22 | # ================================= 23 | # Additional rich-specific helpers 24 | # ================================= 25 | 26 | 27 | def dark(): 28 | """Synonym of set_formatter('dark') designed to be used 29 | within iPython/Jupyter programming environments. 30 | """ 31 | set_formatter("dark") 32 | 33 | 34 | def disable(): 35 | """Disable friendly's exception hook, restoring the previous one""" 36 | if not session.installed: 37 | print(_("Friendly is already disabled.")) 38 | return 39 | try: 40 | get_ipython() # noqa 41 | except NameError: 42 | session.uninstall() 43 | return 44 | from .ipython_common import excepthook 45 | 46 | excepthook.disable() 47 | 48 | 49 | def enable(): 50 | """Enable friendly's exception hook.""" 51 | if session.installed: 52 | print(_("Friendly is already enabled.")) 53 | try: 54 | get_ipython() # noqa 55 | except NameError: 56 | session.install() 57 | return 58 | from .ipython_common import excepthook 59 | 60 | excepthook.enable() 61 | 62 | 63 | def light(): 64 | """Synonym of set_formatter('light') designed to be used 65 | within iPython/Jupyter programming environments. 66 | """ 67 | set_formatter("light") 68 | 69 | 70 | def plain(): 71 | """Synonym of set_formatter('plain'). 72 | Monochrome output without using Rich. 73 | """ 74 | set_formatter("plain") 75 | 76 | 77 | def set_background(color=None): 78 | """Sets the background color for the current environment.""" 79 | if color is None: 80 | colours.set_background_color(None) 81 | return 82 | set_formatter(background=color) 83 | 84 | 85 | def set_highlight(bg="#cc0000", fg="white"): 86 | """Sets the highlight colour. Use None to turn off highlight.""" 87 | # Need to validate colour if not None, and revert to default 88 | colours.set_highlight(bg=bg, fg=fg) 89 | 90 | 91 | def set_width(width=80): 92 | """Sets the width in a iPython/Jupyter session using a Rich formatter.""" 93 | try: 94 | session.console.width = width 95 | except Exception: # noqa 96 | return 97 | session.rich_width = width 98 | if session.is_jupyter and ( 99 | session.rich_tb_width is not None and session.rich_width > session.rich_tb_width 100 | ): 101 | session.rich_tb_width = width 102 | 103 | 104 | short_description["dark"] = lambda: _( 105 | "Sets a colour scheme designed for a black background." 106 | ) 107 | short_description["disable"] = lambda: _("Disable friendly's exception hook.") 108 | short_description["enable"] = lambda: _("Enable friendly's exception hook.") 109 | short_description["light"] = lambda: _( 110 | "Sets a colour scheme designed for a white background." 111 | ) 112 | short_description["plain"] = lambda: _("Plain formatting, with no colours added.") 113 | short_description["set_background"] = lambda: _("Sets the background color.") 114 | short_description["set_highlight"] = lambda: _("Sets the highlight colors; bg and fg.") 115 | short_description["set_width"] = lambda: _("Sets the output width in some modes.") 116 | short_description["_print_settings"] = lambda: _("Prints the saved settings.") 117 | short_description["_remove_environment"] = lambda: ( 118 | "Deletes an environment from the saved settings; default: current environment." 119 | ) 120 | local_helpers = { 121 | "dark": dark, 122 | "disable": disable, 123 | "enable": enable, 124 | "light": light, 125 | "plain": plain, 126 | "set_width": set_width, 127 | "set_background": set_background, 128 | "set_highlight": set_highlight, 129 | "_print_settings": _print_settings, 130 | "_remove_environment": _remove_environment, 131 | } 132 | add_help_attribute(local_helpers) 133 | for helper in local_helpers: 134 | Friendly.add_helper(local_helpers[helper]) 135 | 136 | helpers.update(local_helpers) 137 | __all__ = list(helpers.keys()) 138 | -------------------------------------------------------------------------------- /friendly/rich_formatters.py: -------------------------------------------------------------------------------- 1 | """ 2 | formatters.py 3 | ============== 4 | 5 | This module currently contains the following formatters 6 | 7 | * ``jupyter_interactive()``: formatter for jupyter notebooks that uses 8 | buttons to show and hide parts of the information instead of 9 | having to type them as functions to be executed. 10 | 11 | * ``jupyter()``: basic formatter for Jupyter notebooks 12 | 13 | * ``markdown()``: This produces an output formatted with Markdown syntax. 14 | 15 | * ``markdown_docs()``: This produces an output formatted Markdown syntax, 16 | but where each header is shifted down by 2 (h1 -> h3, etc.) so that they 17 | can be inserted in a document, without creating artificial top headers. 18 | 19 | * ``rich_markdown()``: This produces an output formatted with Markdown syntax, 20 | with some modification, with the end result intended to be printed 21 | in colour in a console using Rich (https://github.com/willmcgugan/rich). 22 | """ 23 | from .my_gettext import current_lang 24 | from friendly_traceback.base_formatters import no_result, repl, select_items 25 | from friendly_traceback.config import session 26 | from friendly_traceback.typing_info import InclusionChoice, Info 27 | from friendly.theme import friendly_pygments 28 | 29 | from pygments import highlight # noqa 30 | from pygments.lexers import PythonLexer, PythonTracebackLexer # noqa 31 | from pygments.formatters import HtmlFormatter # noqa 32 | 33 | from rich import jupyter as rich_jupyter 34 | from rich.markdown import Markdown 35 | from rich.panel import Panel 36 | 37 | ipython_available = False 38 | try: # pragma: no cover 39 | 40 | from IPython.display import display, HTML # noqa 41 | 42 | ipython_available = True 43 | except ImportError: 44 | display = HTML = lambda x: x 45 | 46 | RICH_HEADER = False # not a constant 47 | WIDE_OUTPUT = False # not a constant 48 | COUNT = 0 # not a constant 49 | 50 | _ = current_lang.translate 51 | 52 | 53 | def jupyter_interactive( 54 | info: Info, include: InclusionChoice = "friendly_tb" 55 | ) -> None: # noqa 56 | """This implements a formatter that inserts buttons in a jupyter notebook 57 | allowing to selectively show what/why/where, instead of 58 | showing the friendly_tb by default.""" 59 | global COUNT 60 | if include != "friendly_tb": 61 | text = _markdown(info, include=include, rich=True) 62 | rich_writer(text) 63 | return 64 | COUNT += 1 65 | session.rich_add_vspace = False 66 | add_message(info, count=COUNT) 67 | if "detailed_tb" in info: 68 | add_detailed_tb = len(info["detailed_tb"]) > 2 69 | else: 70 | add_detailed_tb = False 71 | add_control(count=COUNT, add_detailed_tb=add_detailed_tb) 72 | add_friendly_tb(info, count=COUNT) 73 | add_interactive_item(info, "what", count=COUNT) 74 | add_interactive_item(info, "why", count=COUNT) 75 | add_interactive_item(info, "where", count=COUNT) 76 | if add_detailed_tb: 77 | add_interactive_item(info, "detailed_tb", count=COUNT) 78 | 79 | 80 | def add_message(info: Info, count: int = -1) -> None: 81 | """Shows the error message. By default, this is the only item shown 82 | other than a button to reveal""" 83 | old_jupyter_html_format = rich_jupyter.JUPYTER_HTML_FORMAT 84 | rich_jupyter.JUPYTER_HTML_FORMAT = ( 85 | "
".format(count=count) 86 | + old_jupyter_html_format 87 | + "
" 88 | ) 89 | text = _markdown(info, include="message", rich=True) 90 | rich_writer(text) 91 | rich_jupyter.JUPYTER_HTML_FORMAT = old_jupyter_html_format 92 | 93 | 94 | def add_friendly_tb(info: Info, count: int = -1) -> None: 95 | """Adds the friendly_tb, hidden by default""" 96 | old_jupyter_html_format = rich_jupyter.JUPYTER_HTML_FORMAT 97 | name = "friendly_tb" 98 | rich_jupyter.JUPYTER_HTML_FORMAT = ( 99 | "" 104 | ) 105 | text = _markdown(info, include="friendly_tb", rich=True) 106 | rich_writer(text) 107 | rich_jupyter.JUPYTER_HTML_FORMAT = old_jupyter_html_format 108 | 109 | 110 | def add_interactive_item(info: Info, name: InclusionChoice, count: int = -1) -> None: 111 | """Adds interactive items (what/why/where) with buttons to toggle 112 | their visibility.""" 113 | old_jupyter_html_format = rich_jupyter.JUPYTER_HTML_FORMAT 114 | 115 | content = """ 127 | 133 | """.format( 134 | name=name, count=count, hide=_("Hide"), btn_style=session.jupyter_button_style 135 | ) 136 | display(HTML(content)) 137 | 138 | rich_jupyter.JUPYTER_HTML_FORMAT = ( 139 | "" 144 | ) 145 | text = _markdown(info, include=name, rich=True) 146 | rich_writer(text) 147 | 148 | rich_jupyter.JUPYTER_HTML_FORMAT = old_jupyter_html_format 149 | 150 | 151 | def add_control(count: int = -1, add_detailed_tb: bool = False) -> None: 152 | """Adds a single button to control the visibility of all other elements.""" 153 | if add_detailed_tb: 154 | btn_detailed_tb = """;var btn_detailed_tb = 155 | document.getElementById('friendly-tb-btn-show-detailed_tb{count}');""".format( 156 | count=count 157 | ) 158 | var_detailed_tb_content = """var detailed_tb_content = 159 | document.getElementById('friendly-tb-detailed_tb-content{count}');""".format( 160 | count=count 161 | ) 162 | show_detailed_tb_button = """btn_detailed_tb.style.display = 'block';""" 163 | hide_detailed_tb_button = """ 164 | btn_detailed_tb.style.display = 'none'; 165 | btn_detailed_tb.textContent = 'detailed_tb()';""" 166 | show_detailed_tb_content = "detailed_tb_content.display = 'block';" 167 | hide_detailed_tb_content = "detailed_tb_content.display = 'none';" 168 | else: 169 | btn_detailed_tb = "" 170 | var_detailed_tb_content = "" 171 | show_detailed_tb_button = "" 172 | hide_detailed_tb_button = "" 173 | show_detailed_tb_content = "" 174 | hide_detailed_tb_content = "" 175 | content = """ 176 | 182 | 222 | """.format( 223 | count=count, 224 | more=_("More ..."), 225 | only=_("Show message only"), 226 | btn_style=session.jupyter_button_style, 227 | btn_detailed_tb=btn_detailed_tb, 228 | var_detailed_tb_content=var_detailed_tb_content, 229 | show_detailed_tb_content=show_detailed_tb_content, 230 | hide_detailed_tb_content=hide_detailed_tb_content, 231 | show_detailed_tb_button=show_detailed_tb_button, 232 | hide_detailed_tb_button=hide_detailed_tb_button, 233 | ) 234 | display(HTML(content)) 235 | 236 | 237 | def rich_writer(text: str) -> None: # pragma: no cover 238 | """Default writer""" 239 | global RICH_HEADER, WIDE_OUTPUT 240 | if session.rich_add_vspace: 241 | session.console.print() 242 | md = Markdown( 243 | text, inline_code_lexer="python", code_theme=friendly_pygments.CURRENT_THEME 244 | ) 245 | if RICH_HEADER: 246 | title = "Traceback" 247 | md = Panel(md, title=title) 248 | RICH_HEADER = False 249 | session.console.print(md) 250 | if WIDE_OUTPUT: 251 | session.console.width = session.rich_width 252 | WIDE_OUTPUT = False 253 | 254 | 255 | def html_escape(text: str) -> str: # pragma: no cover 256 | if not text: 257 | return "" 258 | text = ( 259 | text.replace("&", "&") 260 | .replace("<", "<") 261 | .replace(">", ">") 262 | .replace("\n\n", "
") 263 | ) 264 | while "`" in text: 265 | text = text.replace("`", "", 1) 266 | text = text.replace("`", "", 1) 267 | return text 268 | 269 | 270 | # For some reason, moving this to friendly.ipython 271 | # and trying to import it from there uninstalls everything: it is as though 272 | # it starts a new iPython subprocess. 273 | def jupyter( 274 | info: Info, include: InclusionChoice = "friendly_tb" 275 | ) -> str: # pragma: no cover 276 | """Jupyter formatter using pygments and html format. 277 | 278 | This can be used as a jupyter theme agnostic formatter as it 279 | works equally well with a light or dark theme. 280 | However, some information shown may be less than optimal 281 | when it comes to visibility/contrast. 282 | """ 283 | css = HtmlFormatter().get_style_defs(".highlight") 284 | display(HTML(f"")) # noqa 285 | items_to_show = select_items(include) 286 | result = False 287 | for item in items_to_show: 288 | if item in info: 289 | result = True 290 | if "source" in item or "variable" in item: 291 | text = info[item] 292 | text = highlight(text, PythonLexer(), HtmlFormatter()) 293 | display(HTML(text)) 294 | elif "traceback" in item: 295 | text = info[item] 296 | text = highlight(text, PythonTracebackLexer(), HtmlFormatter()) 297 | display(HTML(text)) 298 | elif "message" in item: # format like last line of traceback 299 | content = info[item].split(":") 300 | error_name = content[0] 301 | message = ":".join(content[1:]) if len(content) > 1 else "" 302 | text = "".join( 303 | [ 304 | '
',
305 |                         error_name,
306 |                         ': ',
307 |                         message,
308 |                         "
", 309 | ] 310 | ) 311 | display(HTML(text)) 312 | elif item == "suggest": 313 | text = html_escape(info[item]) 314 | display(HTML(f"

{text}

")) 315 | else: 316 | text = html_escape(info[item]) 317 | if "header" in item: 318 | display(HTML(f"

{text}

")) 319 | else: 320 | display(HTML(f'

{text}

')) 321 | if not result: 322 | text = no_result(info, include) 323 | if text: 324 | display(HTML(f'

{text}

')) 325 | return "" 326 | 327 | 328 | if not ipython_available: 329 | jupyter = repl # noqa 330 | 331 | 332 | def markdown( 333 | info: Info, include: InclusionChoice = "friendly_tb" 334 | ) -> str: # pragma: no cover 335 | """Traceback formatted with Markdown syntax. 336 | 337 | Some minor changes of the traceback info content are done, 338 | for nicer final display when the markdown generated content 339 | if further processed. 340 | """ 341 | return _markdown(info, include) 342 | 343 | 344 | def markdown_docs( 345 | info: Info, include: InclusionChoice = "explain" 346 | ) -> str: # pragma: no cover 347 | """Traceback formatted with Markdown syntax, where each 348 | header is shifted down by 2 (h1 -> h3, etc.) so that they 349 | can be inserted in a document, without creating artificial 350 | top headers. 351 | 352 | Some minor changes of the traceback info content are done, 353 | for nicer final display when the markdown generated content 354 | is further processed. 355 | """ 356 | return _markdown(info, include, documentation=True) 357 | 358 | 359 | def rich_markdown( 360 | info: Info, include: InclusionChoice = "friendly_tb" 361 | ) -> str: # pragma: no cover 362 | """Traceback formatted with Markdown syntax suitable for 363 | printing in color in the console using Rich. 364 | 365 | Some minor changes of the traceback info content are done, 366 | for nicer final display when the markdown generated content 367 | if further processed. 368 | 369 | Some additional processing is done just prior to doing the 370 | final output, by ``session._write_err()``. 371 | """ 372 | return _markdown(info, include, rich=True) 373 | 374 | 375 | def detailed_tb(info: Info) -> str: # Special case 376 | # TODO: document this 377 | if "detailed_tb" not in info: 378 | return "" 379 | markdown_items = { 380 | "source": ("```python\n", "\n```"), 381 | "var_info": ("```python\n", "\n```"), 382 | } 383 | result = [""] 384 | for location, source, var_info in info["detailed_tb"]: 385 | result.append(location) 386 | prefix, suffix = markdown_items["source"] 387 | result.append(prefix + source + suffix) 388 | if var_info: 389 | prefix, suffix = markdown_items["var_info"] 390 | result.append(prefix + var_info + suffix) 391 | result.append("\n") 392 | return "\n".join(result) 393 | 394 | 395 | def _markdown( 396 | info: Info, 397 | include: InclusionChoice, 398 | rich: bool = False, 399 | documentation: bool = False, 400 | ) -> str: # pragma: no cover 401 | """Traceback formatted with Markdown syntax.""" 402 | global RICH_HEADER, WIDE_OUTPUT 403 | if include == "detailed_tb" and "detailed_tb" in info: 404 | return detailed_tb(info) 405 | elif include == "detailed_tb": 406 | return "" 407 | if ( 408 | rich 409 | and session.is_jupyter 410 | and session.rich_tb_width is not None 411 | and session.rich_tb_width != session.rich_width 412 | and include in ["friendly_tb", "python_tb", "debug_tb", "where", "explain"] 413 | ): 414 | session.console.width = session.rich_tb_width 415 | WIDE_OUTPUT = True 416 | markdown_items = { 417 | "header": ("# ", ""), 418 | "message": ("", ""), 419 | "suggest": ("", "\n"), 420 | "warning_message": ("", "\n"), 421 | "exception_notes_intro": ("#### ", ""), 422 | "exception_notes": ("", ""), 423 | "generic": ("", ""), 424 | "parsing_error": ("", ""), 425 | "parsing_error_source": ("```python\n", "\n```"), 426 | "cause": ("", ""), 427 | "last_call_header": ("## ", ""), 428 | "last_call_source": ("```python\n", "\n```"), 429 | "last_call_variables": ("```python\n", "\n```"), 430 | "exception_raised_header": ("## ", ""), 431 | "exception_raised_source": ("```python\n", "\n```"), 432 | "exception_raised_variables": ("```python\n", "\n```"), 433 | "simulated_python_traceback": ("```pytb\n", "\n```"), 434 | "original_python_traceback": ("```pytb\n", "\n```"), 435 | "shortened_traceback": ("```pytb\n", "\n```"), 436 | "warning_location_header": ("#### ", ""), 437 | "warning_source": ("```python\n", "\n```"), 438 | "warning_variables": ("```python\n", "\n```"), 439 | "additional_variable_warning": ("#### ", ""), 440 | } 441 | 442 | items_to_show = select_items(include) # tb_items_to_show(level=level) 443 | if rich and include == "explain": 444 | RICH_HEADER = True # Skip it here; handled by session.py 445 | result = [""] 446 | for item in items_to_show: 447 | if item not in markdown_items: 448 | print( 449 | _( 450 | "Inconsistent values: {item} is not present in markdown_items.\n" 451 | "Are you using the latest versions of friendly and friendly_traceback?\n" 452 | "If not, upgrade both packages, otherwise, please report the issue.\n" 453 | ).format(item=item) 454 | ) 455 | continue 456 | prefix, suffix = markdown_items[item] 457 | if item == "exception_notes" and item in info: 458 | lines = [] 459 | for note in info[item]: 460 | note_lines = note.split("\n") 461 | for index, line in enumerate(note_lines): 462 | if index == 0: 463 | note_lines[0] = "* " + note_lines[0] 464 | else: 465 | note_lines[index] = " " + note_lines[index] 466 | note_lines.append("\n") 467 | 468 | lines.extend(note_lines) 469 | lines.append("\n") 470 | content = "".join(lines) 471 | result.append(prefix + content + suffix) 472 | elif item in info and info[item].strip(): 473 | # With normal Markdown formatting, it does not make sense to have a 474 | # header end with a colon. 475 | # However, we style headers differently with Rich; see 476 | # Rich theme in file friendly_rich. 477 | content = info[item] 478 | if item.endswith("header"): 479 | content = ( 480 | content.rstrip(":") 481 | .replace("' ", "'` ") 482 | .replace(" '", " `'") 483 | .replace("'.", "'`.") 484 | ) 485 | if item == "message" and rich: 486 | # Ensure that the exception name is highlighted. 487 | content = content.split(":") 488 | content[0] = "`" + content[0] + "`" 489 | content = ":".join(content) 490 | 491 | if "header" in item and "[" in content: 492 | content = content.replace("[", "`[").replace("]", "]`") 493 | 494 | if item == "parsing_error" and "[" in content: 495 | content = content.replace("[", "`[").replace("]", "]`") 496 | 497 | if documentation and prefix.startswith("#"): 498 | prefix = "##" + prefix 499 | result.append(prefix + content + suffix) 500 | 501 | if result == [""]: 502 | return no_result(info, include) 503 | 504 | if include == "message": 505 | return result[1] 506 | 507 | return "\n\n".join(result) 508 | -------------------------------------------------------------------------------- /friendly/settings.py: -------------------------------------------------------------------------------- 1 | """Settings file exclusively for friendly -- not for friendly-traceback""" 2 | 3 | import configparser 4 | import locale 5 | import os 6 | import sys 7 | 8 | from friendly_traceback import debug_helper 9 | from friendly_traceback.config import session 10 | import platformdirs 11 | 12 | 13 | config_dir = platformdirs.user_config_dir( 14 | appname="FriendlyTraceback", appauthor=False # noqa 15 | ) 16 | FILENAME = os.path.join(config_dir, "friendly.ini") 17 | 18 | # I tried to install psutil to determine the kind of terminal (if any) 19 | # that friendly was running in, but the installation failed. 20 | # So I resort to a simpler way of determining the terminal if possible. 21 | # For terminals, the assumption is that a single background color will 22 | # be used for a given terminal type. 23 | 24 | if "TERM_PROGRAM" in os.environ: # used by VS Code 25 | terminal_type = os.environ["TERM_PROGRAM"] 26 | elif "TERMINAL_EMULATOR" in os.environ: # used by PyCharm 27 | terminal_type = os.environ["TERMINAL_EMULATOR"] 28 | elif "TERM" in os.environ: # Unix? 29 | terminal_type = os.environ["TERM"] 30 | elif sys.platform == "win32": 31 | # The following might be used to distinguish between powershell and cmd. 32 | _ps = len(os.getenv("PSModulePath", "").split(os.pathsep)) 33 | terminal_type = f"win32-{_ps}" 34 | else: 35 | terminal_type = "sys.platform" 36 | 37 | ENVIRONMENT = terminal_type 38 | # ENVIRONMENT is determined by the "flavour" of friendly. 39 | # If a terminal type is identified, then the ENVIRONMENT variable 40 | # would usually be a combination of the terminal_type and the flavour. 41 | 42 | # Perhaps friendly will be used in environments where the user cannot 43 | # create settings directories and files 44 | 45 | 46 | def ensure_existence(): 47 | """Ensures that a settings file exists""" 48 | if not os.path.exists(config_dir): 49 | os.makedirs(config_dir) 50 | if not os.path.exists(FILENAME): 51 | config = configparser.ConfigParser() 52 | with open(FILENAME, "w") as config_file: 53 | config.write(config_file) 54 | 55 | 56 | try: 57 | ensure_existence() 58 | except Exception: # noqa 59 | FILENAME = None 60 | 61 | 62 | def read(*, option="unknown", environment=None): 63 | """Returns the value of a key in the current environment""" 64 | if FILENAME is None: 65 | return getattr(session, option) if hasattr(session, option) else None 66 | if environment is not None: 67 | section = environment 68 | elif ENVIRONMENT is not None: 69 | section = ENVIRONMENT 70 | else: 71 | section = "unknown" 72 | debug_helper.log(f"Reading unknown section: {option}") 73 | config = configparser.ConfigParser() 74 | config.read(FILENAME) 75 | if section in config and option in config[section]: 76 | return config[section][option] 77 | return 78 | 79 | 80 | def write(*, option="unknown", value="unknown", environment=None): 81 | """Updates the value of a key in the current environment. 82 | 83 | If the section does not already exist, it is created. 84 | """ 85 | if not isinstance(option, str): 86 | debug_helper.log(f"option = {option} is not a string.") 87 | return 88 | if not isinstance(value, str): 89 | debug_helper.log(f"value = {value} is not a string.") 90 | return 91 | if FILENAME is None: 92 | setattr(session, option, value) 93 | return 94 | if environment is not None: 95 | section = environment 96 | elif ENVIRONMENT is not None: 97 | section = ENVIRONMENT 98 | else: 99 | section = "unknown" 100 | debug_helper.log(f"writing unknown for environment={environment}") 101 | 102 | config = configparser.ConfigParser() 103 | config.read(FILENAME) 104 | if not config.has_section(section): 105 | config.add_section(section) 106 | config[section][option] = value 107 | with open(FILENAME, "w") as config_file: 108 | config.write(config_file) 109 | 110 | 111 | def _remove_environment(environment=None): 112 | """Removes an environment (option) previously saved.""" 113 | if FILENAME is None: 114 | return 115 | if environment is not None: 116 | section = environment 117 | elif ENVIRONMENT is not None: 118 | section = ENVIRONMENT 119 | else: 120 | print("No environment defined.") 121 | return 122 | config = configparser.ConfigParser() 123 | config.read(FILENAME) 124 | config.remove_section(section) 125 | with open(FILENAME, "w") as config_file: 126 | config.write(config_file) 127 | 128 | 129 | def has_environment(environment=None): 130 | """Returns True if a section in the settings file has already been set 131 | for this environment. 132 | """ 133 | if FILENAME is None: 134 | return False 135 | section = environment if environment is not None else ENVIRONMENT 136 | config = configparser.ConfigParser() 137 | config.read(FILENAME) 138 | return config.has_section(section) 139 | 140 | 141 | def print_settings(): 142 | """Prints the contents of the settings file""" 143 | config = configparser.ConfigParser() 144 | print("Current environment: ", ENVIRONMENT) 145 | config.read(FILENAME) 146 | config.write(sys.stdout) 147 | 148 | 149 | def get_lang() -> str: 150 | lang = read(option="lang", environment="common") 151 | if lang is None: 152 | lang = locale.getlocale()[0] 153 | return lang 154 | -------------------------------------------------------------------------------- /friendly/theme/__init__.py: -------------------------------------------------------------------------------- 1 | """Syntax colouring based on the availability of pygments 2 | """ 3 | import sys 4 | 5 | from . import friendly_pygments 6 | from . import friendly_rich 7 | from . import patch_tb_lexer # noqa will automatically Monkeypatch 8 | from .colours import validate_color 9 | 10 | 11 | def init_rich_console( 12 | style="dark", color_system="auto", force_jupyter=None, background=None 13 | ): 14 | try: 15 | background = validate_color(background) 16 | except ValueError as e: 17 | print(e.args[0]) 18 | background = None 19 | 20 | if style == "light": 21 | theme = friendly_pygments.friendly_light 22 | if background is not None: 23 | friendly_pygments.friendly_light.background_color = background 24 | else: 25 | theme = friendly_pygments.friendly_dark 26 | if background is not None: 27 | friendly_pygments.friendly_dark.background_color = background 28 | friendly_pygments.CURRENT_THEME = theme 29 | 30 | return friendly_rich.init_console( 31 | theme=theme, color_system=color_system, force_jupyter=force_jupyter 32 | ) 33 | 34 | 35 | def disable_rich(): 36 | try: # are we using IPython ? 37 | # get_ipython is an IPython builtin which returns the current instance. 38 | ip = get_ipython() # noqa 39 | from IPython.core.formatters import PlainTextFormatter # noqa 40 | 41 | ip.display_formatter.formatters["text/plain"] = PlainTextFormatter() 42 | except NameError: 43 | sys.displayhook = sys.__displayhook__ 44 | -------------------------------------------------------------------------------- /friendly/theme/colours.py: -------------------------------------------------------------------------------- 1 | from . import friendly_pygments 2 | from ..my_gettext import current_lang 3 | from .. import settings 4 | 5 | 6 | def validate_color(color): 7 | _ = current_lang.translate 8 | if color is None or color == "None": 9 | return None 10 | if not isinstance(color, str): 11 | raise ValueError(_("`color` must be a string.")) 12 | 13 | valid_characters = set("0123456789abcdef") 14 | color = color.lower().replace(" ", "") 15 | 16 | if color in NAMED_COLOURS: 17 | return NAMED_COLOURS[color] 18 | elif ( 19 | color.startswith("#") 20 | and len(color) == 7 21 | and set(color[1:]).issubset(valid_characters) 22 | ): 23 | return color 24 | raise ValueError(_("Invalid color {color}").format(color=color)) 25 | 26 | 27 | def set_background_color(color): 28 | if color is None: 29 | color = friendly_pygments.set_pygments_background_color(None) 30 | settings.write(option="background", value=color) 31 | return color 32 | 33 | try: 34 | validate_color(color) 35 | except ValueError as e: 36 | print(e.args[0]) 37 | return settings.read(option="background") 38 | else: 39 | color = friendly_pygments.set_pygments_background_color(color) 40 | settings.write(option="background", value=color) 41 | return color 42 | 43 | 44 | def set_highlight(bg="#cc0000", fg="white"): 45 | if bg is None or fg is None: 46 | settings.write(option="highlight", value="use carets") 47 | return 48 | try: 49 | fg = validate_color(fg) 50 | except ValueError as e: 51 | print(e.args[0]) 52 | return 53 | try: 54 | bg = validate_color(bg) 55 | except ValueError as e: 56 | print(e.args[0]) 57 | return 58 | 59 | # use Rich format to save value 60 | settings.write(option="highlight", value=f"{fg} on {bg}") 61 | 62 | 63 | def get_highlight(): 64 | highlight = settings.read(option="highlight") 65 | if highlight == "use carets": 66 | return None 67 | elif highlight is None: 68 | bg, fg = friendly_pygments.get_pygments_error_token() 69 | return f"{fg} on {bg}" 70 | return highlight 71 | 72 | 73 | NAMED_COLOURS = { 74 | "aliceblue": "#F0F8FF", 75 | "antiquewhite": "#FAEBD7", 76 | "aqua": "#00FFFF", 77 | "aquamarine": "#7FFFD4", 78 | "azure": "#F0FFFF", 79 | "beige": "#F5F5DC", 80 | "bisque": "#FFE4C4", 81 | "black": "#000000", 82 | "blanchedalmond": "#FFEBCD", 83 | "blue": "#0000FF", 84 | "blueviolet": "#8A2BE2", 85 | "brown": "#A52A2A", 86 | "burlywood": "#DEB887", 87 | "cadetblue": "#5F9EA0", 88 | "chartreuse": "#7FFF00", 89 | "chocolate": "#D2691E", 90 | "coral": "#FF7F50", 91 | "cornflowerblue": "#6495ED", 92 | "cornsilk": "#FFF8DC", 93 | "crimson": "#DC143C", 94 | "cyan": "#00FFFF", 95 | "darkblue": "#00008B", 96 | "darkcyan": "#008B8B", 97 | "darkgoldenrod": "#B8860B", 98 | "darkgray": "#A9A9A9", 99 | "darkgreen": "#006400", 100 | "darkgrey": "#A9A9A9", 101 | "darkkhaki": "#BDB76B", 102 | "darkmagenta": "#8B008B", 103 | "darkolivegreen": "#556B2F", 104 | "darkorange": "#FF8C00", 105 | "darkorchid": "#9932CC", 106 | "darkred": "#8B0000", 107 | "darksalmon": "#E9967A", 108 | "darkseagreen": "#8FBC8F", 109 | "darkslateblue": "#483D8B", 110 | "darkslategray": "#2F4F4F", 111 | "darkslategrey": "#2F4F4F", 112 | "darkturquoise": "#00CED1", 113 | "darkviolet": "#9400D3", 114 | "deeppink": "#FF1493", 115 | "deepskyblue": "#00BFFF", 116 | "dimgray": "#696969", 117 | "dimgrey": "#696969", 118 | "dodgerblue": "#1E90FF", 119 | "firebrick": "#B22222", 120 | "floralwhite": "#FFFAF0", 121 | "forestgreen": "#228B22", 122 | "fuchsia": "#FF00FF", 123 | "gainsboro": "#DCDCDC", 124 | "ghostwhite": "#F8F8FF", 125 | "gold": "#FFD700", 126 | "goldenrod": "#DAA520", 127 | "gray": "#808080", 128 | "green": "#008000", 129 | "greenyellow": "#ADFF2F", 130 | "grey": "#808080", 131 | "honeydew": "#F0FFF0", 132 | "hotpink": "#FF69B4", 133 | "indianred": "#CD5C5C", 134 | "indigo": "#4B0082", 135 | "ivory": "#FFFFF0", 136 | "khaki": "#F0E68C", 137 | "lavender": "#E6E6FA", 138 | "lavenderblush": "#FFF0F5", 139 | "lawngreen": "#7CFC00", 140 | "lemonchiffon": "#FFFACD", 141 | "lightblue": "#ADD8E6", 142 | "lightcoral": "#F08080", 143 | "lightcyan": "#E0FFFF", 144 | "lightgoldenrodyellow": "#FAFAD2", 145 | "lightgray": "#D3D3D3", 146 | "lightgreen": "#90EE90", 147 | "lightgrey": "#D3D3D3", 148 | "lightpink": "#FFB6C1", 149 | "lightsalmon": "#FFA07A", 150 | "lightseagreen": "#20B2AA", 151 | "lightskyblue": "#87CEFA", 152 | "lightslategray": "#778899", 153 | "lightslategrey": "#778899", 154 | "lightsteelblue": "#B0C4DE", 155 | "lightyellow": "#FFFFE0", 156 | "lime": "#00FF00", 157 | "limegreen": "#32CD32", 158 | "linen": "#FAF0E6", 159 | "magenta": "#FF00FF", 160 | "maroon": "#800000", 161 | "mediumaquamarine": "#66CDAA", 162 | "mediumblue": "#0000CD", 163 | "mediumorchid": "#BA55D3", 164 | "mediumpurple": "#9370DB", 165 | "mediumseagreen": "#3CB371", 166 | "mediumslateblue": "#7B68EE", 167 | "mediumspringgreen": "#00FA9A", 168 | "mediumturquoise": "#48D1CC", 169 | "mediumvioletred": "#C71585", 170 | "midnightblue": "#191970", 171 | "mintcream": "#F5FFFA", 172 | "mistyrose": "#FFE4E1", 173 | "moccasin": "#FFE4B5", 174 | "navajowhite": "#FFDEAD", 175 | "navy": "#000080", 176 | "oldlace": "#FDF5E6", 177 | "olive": "#808000", 178 | "olivedrab": "#6B8E23", 179 | "orange": "#FFA500", 180 | "orangered": "#FF4500", 181 | "orchid": "#DA70D6", 182 | "palegoldenrod": "#EEE8AA", 183 | "palegreen": "#98FB98", 184 | "paleturquoise": "#AFEEEE", 185 | "palevioletred": "#DB7093", 186 | "papayawhip": "#FFEFD5", 187 | "peachpuff": "#FFDAB9", 188 | "peru": "#CD853F", 189 | "pink": "#FFC0CB", 190 | "plum": "#DDA0DD", 191 | "powderblue": "#B0E0E6", 192 | "purple": "#800080", 193 | "rebeccapurple": "#663399", 194 | "red": "#FF0000", 195 | "rosybrown": "#BC8F8F", 196 | "royalblue": "#4169E1", 197 | "saddlebrown": "#8B4513", 198 | "salmon": "#FA8072", 199 | "sandybrown": "#F4A460", 200 | "seagreen": "#2E8B57", 201 | "seashell": "#FFF5EE", 202 | "sienna": "#A0522D", 203 | "silver": "#C0C0C0", 204 | "skyblue": "#87CEEB", 205 | "slateblue": "#6A5ACD", 206 | "slategray": "#708090", 207 | "slategrey": "#708090", 208 | "snow": "#FFFAFA", 209 | "springgreen": "#00FF7F", 210 | "steelblue": "#4682B4", 211 | "tan": "#D2B48C", 212 | "teal": "#008080", 213 | "thistle": "#D8BFD8", 214 | "tomato": "#FF6347", 215 | "turquoise": "#40E0D0", 216 | "violet": "#EE82EE", 217 | "wheat": "#F5DEB3", 218 | "white": "#FFFFFF", 219 | "whitesmoke": "#F5F5F5", 220 | "yellow": "#FFFF00", 221 | "yellowgreen": "#9ACD32", 222 | } 223 | -------------------------------------------------------------------------------- /friendly/theme/friendly_pygments.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pygments import styles 4 | from pygments.token import Error 5 | from friendly_styles import friendly_light, friendly_dark 6 | 7 | # When friendly is imported in environments that have previously 8 | # imported Pygments, the styles defined in friendly_styles do not 9 | # get automatically added to the list of available styles from pygments, 10 | # and we must "patch" the existing list. 11 | sys.modules["pygments.styles.friendly_light"] = friendly_light 12 | styles.STYLE_MAP["friendly_light"] = "friendly_light::FriendlyLightStyle" 13 | friendly_light = styles.get_style_by_name("friendly_light") 14 | 15 | sys.modules["pygments.styles.friendly_dark"] = friendly_dark 16 | styles.STYLE_MAP["friendly_dark"] = "friendly_dark::FriendlyDarkStyle" 17 | friendly_dark = styles.get_style_by_name("friendly_dark") 18 | 19 | # The following global variable can be changed from other modules. 20 | # Yes, I know, global variables are not a good idea. 21 | CURRENT_THEME = friendly_dark 22 | 23 | default_dark_background_colour = friendly_dark.background_color 24 | default_light_background_colour = friendly_light.background_color 25 | 26 | default_dark_highlight_colour = friendly_dark.styles[Error] 27 | default_light_highlight_colour = friendly_light.styles[Error] 28 | 29 | 30 | def get_default_background_color(): 31 | if CURRENT_THEME == friendly_dark: 32 | return default_dark_background_colour 33 | else: 34 | return default_light_background_colour 35 | 36 | 37 | def set_pygments_background_color(color): 38 | if color is None: 39 | CURRENT_THEME.background_color = get_default_background_color() 40 | return get_default_background_color() 41 | 42 | CURRENT_THEME.background_color = color 43 | return color 44 | 45 | 46 | def set_pygments_error_token(bg, fg): 47 | CURRENT_THEME.styles[Error] = f"bg:{bg} {fg}" 48 | 49 | 50 | def get_pygments_error_token(): 51 | if CURRENT_THEME == friendly_dark: 52 | colour = default_dark_highlight_colour 53 | else: 54 | colour = default_light_highlight_colour 55 | bg, fg = colour.split(" ") 56 | bg = bg[3:] 57 | return bg, fg 58 | -------------------------------------------------------------------------------- /friendly/theme/friendly_rich.py: -------------------------------------------------------------------------------- 1 | """Friendly-rich 2 | 3 | All Rich-related imports and redefinitions are done here. 4 | 5 | """ 6 | import builtins 7 | import inspect 8 | 9 | from .friendly_pygments import friendly_dark, friendly_light 10 | from . import colours 11 | from friendly_traceback.utils import get_highlighting_ranges 12 | 13 | import rich 14 | from rich import pretty 15 | from rich.markdown import Heading, CodeBlock 16 | from rich.syntax import Syntax 17 | from rich.text import Text 18 | from rich.theme import Theme 19 | 20 | from pygments.token import Comment, Generic, Keyword, Name, Number, Operator, String 21 | 22 | from friendly_traceback import token_utils 23 | 24 | dark_background_theme = Theme(friendly_dark.friendly_style) 25 | light_background_theme = Theme(friendly_light.friendly_style) 26 | 27 | 28 | def is_builtin(string): 29 | if string.strip() not in dir(builtins): 30 | return False 31 | try: 32 | return inspect.isbuiltin(eval(string.strip())) 33 | except: # noqa 34 | return False 35 | 36 | 37 | def is_exception(string): 38 | if string.strip() not in dir(builtins): 39 | return False 40 | try: 41 | return issubclass(eval(string.strip()), BaseException) 42 | except: # noqa 43 | return False 44 | 45 | 46 | class MultilineString: 47 | def __init__(self, begin_col=None, begin_line=None, end_col=None, end_line=None): 48 | self.begin_col = begin_col 49 | self.begin_line = begin_line - 1 50 | self.end_col = end_col 51 | self.end_line = end_line - 1 52 | 53 | 54 | class ColourHighlighter: 55 | """This class is intended to take the output from friendly traceback 56 | with typical line numbering and highlighting with carets, i.e. something 57 | like: 58 | 1| import math 59 | -->2| print(math.Pi) 60 | ^^ 61 | 3| ... 62 | 63 | And add syntax colouring and replacing the carets ^ by special highlight 64 | of the line above. 65 | """ 66 | 67 | def __init__(self, theme): 68 | self.theme = theme 69 | background = theme.background_color 70 | self.operator_style = f"{theme.styles[Operator]} on {background}" 71 | self.number_style = f"{theme.styles[Number]} on {background}" 72 | self.code_style = f"{theme.styles[Name]} on {background}" 73 | self.keyword_style = f"{theme.styles[Keyword]} on {background}" 74 | self.constant_style = f"{theme.styles[Keyword.Constant]} on {background}" 75 | self.comment_style = f"{theme.styles[Comment]} on {background}" 76 | self.builtin_style = f"{theme.styles[Name.Builtin]} on {background}" 77 | self.exception_style = f"{theme.styles[Generic.Error]} on {background}" 78 | self.string_style = f"{theme.styles[String]} on {background}" 79 | self.error_style = colours.get_highlight() 80 | 81 | def split_lineno_from_code(self, lines): 82 | # Also remove lines of markers 83 | self.lineno_info = [] 84 | self.code_lines = [] 85 | for line in lines: 86 | if ( 87 | line.find("|") == -1 88 | and set(line.strip()) != {"^"} 89 | and line.strip() != ":" 90 | ): 91 | self.end_lineno_marker = 0 92 | break 93 | else: 94 | self.end_lineno_marker = lines[0].find("|") + 1 95 | for line in lines: 96 | lineno_marker = line[0 : self.end_lineno_marker] 97 | if lineno_marker.strip(): 98 | self.lineno_info.append(lineno_marker) 99 | self.code_lines.append((line[self.end_lineno_marker :])) 100 | elif set(line.strip()) != {"^"}: 101 | self.lineno_info.append("") 102 | self.code_lines.append(line) 103 | 104 | def shift_error_lines(self, error_lines): 105 | new_error_lines = {} 106 | line_numbers = sorted(list(error_lines.keys())) 107 | up_shift = 1 108 | for lineno in line_numbers: 109 | new_error_lines[lineno - up_shift] = [] 110 | for (begin, end) in error_lines[lineno][1::2]: 111 | new_error_lines[lineno - up_shift].append( 112 | ( 113 | max(0, begin - self.end_lineno_marker), 114 | end - self.end_lineno_marker, 115 | ) 116 | ) 117 | up_shift += 1 118 | return new_error_lines 119 | 120 | def find_multiline_strings(self): 121 | self.multiline_strings = [] 122 | source = "\n".join(self.code_lines) 123 | tokens = token_utils.tokenize(source) 124 | multiline_string = None 125 | for index, token in enumerate(tokens): 126 | if multiline_string: 127 | if multiline_string.end_line == token.start_row: 128 | multiline_string.end_col = token.start_col 129 | self.multiline_strings.append(multiline_string) 130 | if token.start_row != token.end_row: 131 | multiline_string = MultilineString( 132 | begin_col=token.start_col, 133 | begin_line=token.start_row, 134 | end_col=token.end_col, 135 | end_line=token.end_row, 136 | ) 137 | if index != 0: 138 | prev_token = tokens[index - 1] 139 | if multiline_string.begin_line == prev_token.end_row: 140 | multiline_string.begin_col = tokens[index - 1].end_col 141 | else: 142 | multiline_string = None 143 | 144 | def format_lineno_info(self, lineno_marker): 145 | if "-->" in lineno_marker: 146 | indent, number = lineno_marker.split("-->") 147 | text = Text(indent + " > ", style=self.operator_style) 148 | return text.append(Text(number, style=self.number_style)) 149 | return Text(lineno_marker, style=self.comment_style) 150 | 151 | def format_code_line(self, new_line, code_line, error_line=None): 152 | if not error_line: 153 | error_line = [] 154 | tokens = token_utils.tokenize(code_line) 155 | if error_line: 156 | return self.format_code_line_with_error(new_line, tokens, error_line) 157 | end_previous = 0 158 | for token in tokens: 159 | if not token.string: 160 | continue 161 | new_line.append(self.highlight_token(token, end_previous)) 162 | end_previous = token.end_col 163 | return new_line 164 | 165 | def format_code_line_with_error(self, new_line, tokens, error_line): 166 | end_previous = 0 167 | for token in tokens: 168 | if not token.string.strip(): 169 | # handle spaces explicitly below to get the alignment right 170 | continue 171 | for begin, end in error_line: 172 | if begin <= token.start_col < end: 173 | if begin > end_previous: 174 | spaces = " " * (begin - end_previous) 175 | new_line.append(spaces) 176 | end_previous = begin 177 | nb_spaces = token.start_col - end_previous 178 | text_string = " " * nb_spaces + token.string 179 | new_line.append(Text(text_string, style=self.error_style)) 180 | break 181 | elif token.start_col <= begin and token.end_col >= end: 182 | # Error is inside token.string; 183 | # For example, highlighting \' in 184 | # 'don\'t' 185 | # ^^ 186 | style = self.get_style(token) 187 | if token.start_col > end_previous: 188 | spaces = " " * (token.start_col - end_previous) 189 | new_line.append(spaces) 190 | tok_string = token.string 191 | text_string = tok_string[: begin - token.start_col] 192 | new_line.append(Text(text_string, style=style)) 193 | text_string = tok_string[ 194 | begin - token.start_col : end - token.start_col 195 | ] 196 | new_line.append(Text(text_string, style=self.error_style)) 197 | text_string = tok_string[end - token.start_col :] 198 | new_line.append(Text(text_string, style=style)) 199 | break 200 | else: 201 | new_line.append(self.highlight_token(token, end_previous)) 202 | end_previous = token.end_col 203 | return new_line 204 | 205 | def format_lines(self, lines, error_lines): 206 | self.split_lineno_from_code(lines) 207 | error_lines = self.shift_error_lines(error_lines) 208 | self.find_multiline_strings() 209 | lineno = -1 210 | new_lines = [] 211 | for lineno_marker, code_line in zip(self.lineno_info, self.code_lines): 212 | lineno += 1 213 | error_line = error_lines[lineno] if lineno in error_lines else None 214 | new_line = self.format_lineno_info(lineno_marker) 215 | 216 | inside_multiline = False 217 | for line_ in self.multiline_strings: 218 | if line_.begin_line < lineno < line_.end_line: 219 | inside_multiline = True 220 | break 221 | elif lineno == line_.end_line: 222 | new_line.append( 223 | Text(code_line[0 : line_.end_col], style=self.string_style) 224 | ) 225 | code_line = code_line[line_.end_col :] 226 | if error_line: 227 | new_error_line = [] 228 | for (begin, end) in error_line: 229 | new_error_line.append( 230 | ( 231 | max(0, begin - line_.end_col), 232 | end - line_.end_col, 233 | ) 234 | ) 235 | error_line = new_error_line 236 | 237 | if inside_multiline: 238 | new_line.append(Text(code_line, style=self.string_style)) 239 | elif error_line: 240 | new_line = self.format_code_line(new_line, code_line, error_line) 241 | else: 242 | new_line = self.format_code_line(new_line, code_line) 243 | new_lines.append(new_line) 244 | return new_lines 245 | 246 | def highlight_token(self, token, end_previous=0): 247 | """Imitating pygment's styling of individual token.""" 248 | nb_spaces = token.start_col - end_previous 249 | text_string = " " * nb_spaces + token.string 250 | style = self.get_style(token) 251 | return Text(text_string, style=style) 252 | 253 | def get_style(self, token): 254 | """Imitating pygment's styling of individual token.""" 255 | text_string = token.string 256 | if token.is_keyword(): 257 | if text_string in ["True", "False", "None"]: 258 | return self.constant_style 259 | else: 260 | return self.keyword_style 261 | elif is_builtin(text_string): 262 | return self.builtin_style 263 | elif is_exception(text_string): 264 | return self.exception_style 265 | elif token.is_comment(): 266 | return self.comment_style 267 | elif token.is_number(): 268 | return self.number_style 269 | elif token.is_operator(): 270 | return self.operator_style 271 | elif token.is_string() or token.is_unclosed_string(): 272 | return self.string_style 273 | else: 274 | return self.code_style 275 | 276 | 277 | def init_console(theme=friendly_dark, color_system="auto", force_jupyter=None): 278 | def _patch_heading(self, *_args): 279 | """By default, all headings are centered by Rich; I prefer to have 280 | them left-justified, except for

281 | """ 282 | text = self.text 283 | text.justify = "left" 284 | # Older version of Rich uses 'level' as an attribute. 285 | # Maintaining compatibility for now. 286 | if (hasattr(self, "level") and self.level == 3) or ( 287 | hasattr(self, "tag") and self.tag == "h3" 288 | ): 289 | yield Text(" ") + text 290 | else: 291 | yield text 292 | 293 | Heading.__rich_console__ = _patch_heading 294 | 295 | def _patch_code_block(self, *_args): 296 | if self.lexer_name != "pytb": 297 | self.lexer_name = "python" 298 | 299 | code = str(self.text).rstrip() 300 | lines = code.split("\n") 301 | error_lines = get_highlighting_ranges(lines) 302 | # Sometimes, an entire line is the cause of an error and is not 303 | # highlighted with carets so that error_lines is an empty dict. 304 | if not error_lines: 305 | for line in lines: 306 | if line.strip().startswith("-->"): 307 | error_lines = {0: tuple()} 308 | break 309 | 310 | if ( 311 | colours.get_highlight() is not None # otherwise, use carets 312 | and self.lexer_name == "python" # do not process pytb 313 | and error_lines 314 | ): 315 | yield from ColourHighlighter(theme).format_lines(lines, error_lines) 316 | else: 317 | yield Syntax(code, self.lexer_name, theme=theme, word_wrap=True) 318 | 319 | CodeBlock.__rich_console__ = _patch_code_block 320 | 321 | if theme == friendly_light: 322 | rich.reconfigure( 323 | theme=light_background_theme, 324 | color_system=color_system, 325 | force_jupyter=force_jupyter, 326 | ) 327 | else: 328 | rich.reconfigure( 329 | theme=dark_background_theme, 330 | color_system=color_system, 331 | force_jupyter=force_jupyter, 332 | ) 333 | console = rich.get_console() 334 | pretty.install(console=console, indent_guides=True) 335 | return console 336 | -------------------------------------------------------------------------------- /friendly/theme/patch_tb_lexer.py: -------------------------------------------------------------------------------- 1 | # Monkeypatching the traceback lexer so that it treats 2 | # "Code block" the same as "File" 3 | from pygments.lexers.python import PythonTracebackLexer 4 | from pygments.lexer import bygroups 5 | from pygments.token import Text, Name, Number, Operator, Generic 6 | 7 | 8 | PythonTracebackLexer.tokens["root"].insert( 9 | 0, 10 | # SyntaxError in interactive interpreter can start with this. 11 | (r"^(?= Code block \[\d+\], line \d+)", Generic.Traceback, "intb"), 12 | ) 13 | 14 | PythonTracebackLexer.tokens["intb"].insert( 15 | 0, 16 | ( 17 | r"^( Code block )(\[)(\d+)(\])(, line )(\d+)(, in )(.+)(\n)", 18 | bygroups(Text, Operator, Number, Operator, Text, Number, Text, Name, Text), 19 | ), 20 | ) 21 | PythonTracebackLexer.tokens["intb"].insert( 22 | 1, 23 | ( 24 | r"^( Code block )(\[)(\d+)(\])(, line )(\d+)(\n)", 25 | bygroups(Text, Operator, Number, Operator, Text, Number, Text), 26 | ), 27 | ) 28 | -------------------------------------------------------------------------------- /friendly_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/friendly_logo.png -------------------------------------------------------------------------------- /images/explain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/images/explain.png -------------------------------------------------------------------------------- /images/friendly_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/images/friendly_logo.png -------------------------------------------------------------------------------- /images/jb_beam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/friendly-traceback/friendly/fe5d3a29c99214b8d97c50ff8e2f217036dd3b55/images/jb_beam.png -------------------------------------------------------------------------------- /manifest.in: -------------------------------------------------------------------------------- 1 | recursive-include friendly/locales *.* 2 | -------------------------------------------------------------------------------- /pypi_upload.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | :Ask 3 | echo Did you update the version?(y/n) 4 | set ANSWER= 5 | set /P ANSWER=Type input: %=% 6 | If /I "%ANSWER%"=="y" goto yes 7 | If /I "%ANSWER%"=="n" goto no 8 | echo Incorrect input & goto Ask 9 | :yes 10 | rd /S /Q dist 11 | rd /S /Q build 12 | rd /S /Q __pycache__ 13 | python setup.py sdist bdist_wheel 14 | twine upload dist/* 15 | :no 16 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.mypy] 6 | files = ["friendly"] 7 | ignore_missing_imports = false 8 | warn_unused_configs = true 9 | disallow_subclassing_any = true 10 | disallow_any_generics = true 11 | disallow_untyped_calls = true 12 | disallow_untyped_defs = true 13 | disallow_incomplete_defs = true 14 | check_untyped_defs = true 15 | disallow_untyped_decorators = true 16 | no_implicit_optional = true 17 | warn_redundant_casts = true 18 | warn_unused_ignores = true 19 | warn_return_any = true 20 | warn_unreachable = true 21 | show_error_codes = true 22 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | rich >= 11 2 | colorama 3 | pygments 4 | friendly-traceback >= 0.7.43 5 | friendly_styles >= 0.2 6 | platformdirs -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = friendly 3 | version = attr: friendly.__version__ 4 | description = Friendlier tracebacks in any language. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | classifiers = 8 | Development Status :: 4 - Beta 9 | Environment :: Console 10 | License :: OSI Approved :: MIT License 11 | Natural Language :: English 12 | Natural Language :: French 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.6 15 | Programming Language :: Python :: 3.7 16 | Programming Language :: Python :: 3.8 17 | Programming Language :: Python :: 3.9 18 | Programming Language :: Python :: 3.10 19 | Intended Audience :: Education 20 | Topic :: Education 21 | Topic :: Software Development :: Interpreters 22 | url = https://github.com/friendly-traceback/friendly 23 | author = Andre Roberge 24 | author_email = Andre.Roberge@gmail.com 25 | 26 | [options] 27 | packages = find: 28 | python_requires = >=3.6.1 29 | include_package_data = True 30 | zip_safe = False 31 | install_requires = 32 | rich >= 11 33 | pygments >= 2.6 34 | friendly-traceback >= 0.7.43 35 | friendly_styles >= 0.2 36 | platformdirs 37 | 38 | [options.packages.find] 39 | exclude = 40 | dist 41 | build 42 | tools 43 | demos 44 | tests 45 | tests.* 46 | *.tests 47 | *.tests.* 48 | venv* 49 | 50 | [options.entry_points] 51 | console_scripts = 52 | friendly = friendly.__main__:main 53 | 54 | [options.package_data] 55 | * = 56 | friendly/locales/* 57 | friendly = py.typed 58 | 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 5 | if parent_dir not in sys.path: 6 | sys.path.insert(0, parent_dir) 7 | 8 | from friendly_traceback import debug_helper 9 | debug_helper.DEBUG = True 10 | -------------------------------------------------------------------------------- /tests/basic_text.out: -------------------------------------------------------------------------------- 1 | 2 | ┌───────────────────────────────── Traceback ─────────────────────────────────┐ 3 | │ Traceback (most recent call last):  │ 4 | │  File "HOME:\github\friendly\tests\test_simple.py", line 16, in test_basic │ 5 | │  a = b # noqa  │ 6 | │ NameError: name 'b' is not defined  │ 7 | │ │ 8 | │ A NameError exception indicates that a variable or function name is not │ 9 | │ known to Python. Most often, this is because there is a spelling mistake. │ 10 | │ However, sometimes it is because the name is used before being defined or │ 11 | │ given a value. │ 12 | │ │ 13 | │ In your program, no object with the name b exists. I have no additional │ 14 | │ information for you. │ 15 | │ │ 16 | │ Exception raised on line 16 of file  │ 17 | │ 'HOME:\github\friendly\tests\test_simple.py'. │ 18 | │ │ 19 | │  13| console = session.console │ 20 | │  14| with console.capture() as capture: │ 21 | │  15| try: │ 22 | │  > 16| a = b # noqa │ 23 | │  17| except NameError: │ 24 | └─────────────────────────────────────────────────────────────────────────────┘ 25 | 26 | -------------------------------------------------------------------------------- /tests/test_simple.py: -------------------------------------------------------------------------------- 1 | import friendly 2 | import friendly_traceback 3 | from friendly_traceback.config import session 4 | 5 | 6 | def test_basic(): 7 | # Make sure that we always get the same results no matter 8 | # what the language settings have been set to 9 | lang = friendly.get_lang() 10 | friendly.set_lang("en") 11 | 12 | friendly.set_formatter("dark", color_system="truecolor") 13 | console = session.console 14 | with console.capture() as capture: 15 | try: 16 | a = b # noqa 17 | except NameError: 18 | friendly_traceback.explain_traceback() 19 | str_output = capture.get() 20 | 21 | with open("tests/basic_text.out", encoding="utf8") as f: 22 | result = f.read() 23 | assert str_output == result 24 | 25 | # Restore the original settings 26 | friendly.set_lang(lang) 27 | -------------------------------------------------------------------------------- /upgrade_all.bat: -------------------------------------------------------------------------------- 1 | call venv-friendly-3.6\scripts\activate 2 | call python -m pip install friendly-traceback --upgrade 3 | call python -m pip install -r requirements.txt --upgrade 4 | call python -m pip install -r requirements-dev.txt --upgrade 5 | call python -m pip install pip --upgrade 6 | 7 | call venv-friendly-3.7\scripts\activate 8 | call python -m pip install friendly-traceback --upgrade 9 | call python -m pip install -r requirements.txt --upgrade 10 | call python -m pip install -r requirements-dev.txt --upgrade 11 | call python -m pip install pip --upgrade 12 | 13 | call venv-friendly-3.8\scripts\activate 14 | call python -m pip install friendly-traceback --upgrade 15 | call python -m pip install -r requirements.txt --upgrade 16 | call python -m pip install -r requirements-dev.txt --upgrade 17 | call python -m pip install pip --upgrade 18 | 19 | call venv-friendly-3.9\scripts\activate 20 | call python -m pip install friendly-traceback --upgrade 21 | call python -m pip install -r requirements.txt --upgrade 22 | call python -m pip install -r requirements-dev.txt --upgrade 23 | call python -m pip install pip --upgrade 24 | 25 | call venv-friendly-3.10\scripts\activate 26 | call python -m pip install friendly-traceback --upgrade 27 | call python -m pip install -r requirements.txt --upgrade 28 | call python -m pip install -r requirements-dev.txt --upgrade 29 | call python -m pip install pip --upgrade 30 | 31 | call venv-friendly-3.11\scripts\activate 32 | call python -m pip install friendly-traceback --upgrade 33 | call python -m pip install -r requirements.txt --upgrade 34 | call python -m pip install -r requirements-dev.txt --upgrade 35 | call python -m pip install pip --upgrade 36 | 37 | call venv-friendly-ipython\scripts\activate 38 | call python -m pip install friendly-traceback --upgrade 39 | call python -m pip install -r requirements.txt --upgrade 40 | call python -m pip install -r requirements-dev.txt --upgrade 41 | call python -m pip install pip --upgrade 42 | 43 | call venv-friendly-3.9\scripts\activate 44 | -------------------------------------------------------------------------------- /upgrade_ft.bat: -------------------------------------------------------------------------------- 1 | call venv-friendly-3.6\scripts\activate 2 | call python -m pip install friendly-traceback --upgrade 3 | 4 | call venv-friendly-3.7\scripts\activate 5 | call python -m pip install friendly-traceback --upgrade 6 | 7 | call venv-friendly-3.8\scripts\activate 8 | call python -m pip install friendly-traceback --upgrade 9 | 10 | call venv-friendly-3.9\scripts\activate 11 | call python -m pip install friendly-traceback --upgrade 12 | 13 | call venv-friendly-3.10\scripts\activate 14 | call python -m pip install friendly-traceback --upgrade 15 | 16 | call venv-friendly-3.11\scripts\activate 17 | call python -m pip install friendly-traceback --upgrade 18 | 19 | call venv-friendly-ipython\scripts\activate 20 | call python -m pip install friendly-traceback --upgrade 21 | 22 | call venv-friendly-3.9\scripts\activate 23 | --------------------------------------------------------------------------------