├── .github └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── images ├── image1.png ├── image10.png ├── image11.png ├── image12.png ├── image13.png ├── image14.png ├── image2.png ├── image3.png ├── image4.png ├── image5.png ├── image6.png ├── image7.png └── image9.png ├── noxfile.py ├── pdbr ├── __init__.py ├── __main__.py ├── _cm.py ├── _console_layout.py ├── _pdbr.py ├── cli.py ├── helpers.py ├── middlewares │ ├── __init__.py │ ├── django.py │ └── starlette.py ├── runner.py └── utils.py ├── poetry.lock ├── pyproject.toml ├── runtests.py ├── scripts ├── lint └── test ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── test_api.py ├── test_config.py ├── test_magic.py ├── test_pdbr.py └── tests_django ├── __init__.py ├── test_settings.py ├── tests.py └── urls.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-n-publish: 11 | name: Build and publish to PyPI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.10.16 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10.16' 19 | - name: Install poetry 20 | run: python -m pip install poetry --user 21 | - name: Build 22 | run: poetry build 23 | - name: Publish to PyPI 24 | env: 25 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_PASSWORD }} 26 | run: poetry publish 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | jobs: 10 | check: 11 | name: "Check" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10' 19 | - name: Install pre-commit 20 | run: | 21 | pip install --upgrade pre-commit 22 | - name: Run check 23 | run: | 24 | pre-commit run --all-files 25 | 26 | test: 27 | name: "Tests" 28 | runs-on: ${{ matrix.platform }} 29 | needs: check 30 | 31 | strategy: 32 | matrix: 33 | platform: [ubuntu-latest, macos-latest, windows-latest] 34 | python-version: ["3.8", "3.9", "3.10", "3.11"] 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Set up Python ${{ matrix.python-version }} 40 | uses: actions/setup-python@v4 41 | with: 42 | python-version: ${{ matrix.python-version }} 43 | 44 | - name: Install nox 45 | run: | 46 | pip install --upgrade nox 47 | - name: Run tests 48 | run: | 49 | nox --sessions test django_test 50 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # lint 141 | .ruff_cache 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-added-large-files 6 | - id: check-case-conflict 7 | - id: check-merge-conflict 8 | - id: check-symlinks 9 | - id: check-toml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/pre-commit/pygrep-hooks 13 | rev: v1.10.0 14 | hooks: 15 | - id: python-check-blanket-noqa 16 | - id: python-use-type-annotations 17 | - repo: https://github.com/jendrikseipp/vulture 18 | rev: v2.14 19 | hooks: 20 | - id: vulture 21 | - repo: https://github.com/astral-sh/ruff-pre-commit 22 | rev: 'v0.9.3' 23 | hooks: 24 | - id: ruff 25 | - repo: https://github.com/psf/black 26 | rev: 24.10.0 27 | hooks: 28 | - id: black 29 | language_version: python3 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "makefile.extensionOutputFolder": "./.vscode" 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.9 2 | 3 | ENV PYTHONUNBUFFERED=0 4 | 5 | RUN pip install pip \ 6 | && pip install nox \ 7 | && pip install pre-commit 8 | 9 | WORKDIR /pdbr 10 | COPY . . 11 | 12 | RUN pre-commit run --all-files 13 | RUN nox --sessions test django_test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Can Sarıgöl 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | sh scripts/lint 3 | 4 | test: 5 | sh scripts/test 6 | 7 | celery: 8 | celery -A tasks worker --loglevel=info 9 | 10 | build: 11 | docker build -t pdbr . 12 | 13 | act: 14 | act -r -j test --container-architecture linux/amd64 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pdbr 2 | 3 | [![PyPI version](https://badge.fury.io/py/pdbr.svg)](https://pypi.org/project/pdbr/) [![Python Version](https://img.shields.io/pypi/pyversions/pdbr.svg)](https://pypi.org/project/pdbr/) [![](https://github.com/cansarigol/pdbr/workflows/Test/badge.svg)](https://github.com/cansarigol/pdbr/actions?query=workflow%3ATest) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/cansarigol/pdbr/master.svg)](https://results.pre-commit.ci/latest/github/cansarigol/pdbr/master) 4 | 5 | 6 | `pdbr` is intended to make the PDB results more colorful. it uses [Rich](https://github.com/willmcgugan/rich) library to carry out that. 7 | 8 | 9 | ## Installing 10 | 11 | Install with `pip` or your favorite PyPi package manager. 12 | 13 | ``` 14 | pip install pdbr 15 | ``` 16 | 17 | 18 | ## Breakpoint 19 | 20 | In order to use ```breakpoint()```, set **PYTHONBREAKPOINT** with "pdbr.set_trace" 21 | 22 | ```python 23 | import os 24 | 25 | os.environ["PYTHONBREAKPOINT"] = "pdbr.set_trace" 26 | ``` 27 | 28 | or just import pdbr 29 | 30 | ```python 31 | import pdbr 32 | ``` 33 | 34 | ## New commands 35 | ### (i)nspect / inspectall | ia 36 | [rich.inspect](https://rich.readthedocs.io/en/latest/introduction.html?s=03#rich-inspector) 37 | ### search | src 38 | Search a phrase in the current frame. 39 | In order to repeat the last one, type **/** character as arg. 40 | ### sql 41 | Display value in sql format. Don't forget to install [sqlparse](https://github.com/andialbrecht/sqlparse) package. 42 | ![](/images/image13.png) 43 | 44 | It can be used for Django model queries as follows. 45 | ``` 46 | >>> sql str(Users.objects.all().query) 47 | ``` 48 | ![](/images/image14.png) 49 | ### (syn)tax 50 | [ val,lexer ] Display [lexer](https://pygments.org/docs/lexers/). 51 | ### (v)ars 52 | Get the local variables list as table. 53 | ### varstree | vt 54 | Get the local variables list as tree. 55 | 56 | ![](/images/image5.png) 57 | 58 | ## Config 59 | Config is specified in **setup.cfg** and can be local or global. Local config (current working directory) has precedence over global (default) one. Global config must be located at `$XDG_CONFIG_HOME/pdbr/setup.cfg`. 60 | 61 | ### Style 62 | In order to use Rich's traceback, style, and theme: 63 | 64 | ``` 65 | [pdbr] 66 | style = yellow 67 | use_traceback = True 68 | theme = friendly 69 | ``` 70 | 71 | Also custom `Console` object can be assigned to the `set_trace`. 72 | ```python 73 | import pdbr 74 | 75 | from rich.console import Console 76 | from rich.style import Style 77 | from rich.theme import Theme 78 | 79 | custom_theme = Theme({ 80 | "info": "dim cyan", 81 | "warning": "magenta", 82 | "danger": "bold red", 83 | }) 84 | custom_style = Style( 85 | color="magenta", 86 | bgcolor="yellow", 87 | italic=True, 88 | ) 89 | console = Console(theme=custom_theme, style=custom_style) 90 | 91 | pdbr.set_trace(console=console) 92 | ``` 93 | ### History 94 | **store_history** setting is used to keep and reload history, even the prompt is closed and opened again: 95 | ``` 96 | [pdbr] 97 | ... 98 | store_history=.pdbr_history 99 | ``` 100 | 101 | By default, history is stored globally in `~/.pdbr_history`. 102 | 103 | ### Context 104 | The **context** setting is used to specify the number of lines of source code context to show when displaying stacktrace information. 105 | ``` 106 | [pdbr] 107 | ... 108 | context=10 109 | ``` 110 | This setting is only available when using `pdbr` with `IPython`. 111 | 112 | ## Celery 113 | In order to use **Celery** remote debugger with pdbr, use ```celery_set_trace``` as below sample. For more information see the [Celery user guide](https://docs.celeryproject.org/en/stable/userguide/debugging.html). 114 | 115 | ```python 116 | from celery import Celery 117 | 118 | app = Celery('tasks', broker='pyamqp://guest@localhost//') 119 | 120 | @app.task 121 | def add(x, y): 122 | 123 | import pdbr; pdbr.celery_set_trace() 124 | 125 | return x + y 126 | 127 | ``` 128 | #### Telnet 129 | Instead of using `telnet` or `nc`, in terms of using pdbr style, `pdbr_telnet` command can be used. 130 | ![](/images/image6.png) 131 | 132 | Also in order to activate history and be able to use arrow keys, install and use [rlwrap](https://github.com/hanslub42/rlwrap) package. 133 | 134 | ``` 135 | rlwrap -H '~/.pdbr_history' pdbr_telnet localhost 6899 136 | ``` 137 | 138 | ## IPython 139 | 140 | `pdbr` integrates with [IPython](https://ipython.readthedocs.io/). 141 | 142 | This makes [`%magics`](https://ipython.readthedocs.io/en/stable/interactive/magics.html) available, for example: 143 | 144 | ```python 145 | (Pdbr) %timeit range(100) 146 | 104 ns ± 2.05 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) 147 | ``` 148 | 149 | To enable `IPython` features, install it separately, or like below: 150 | 151 | ``` 152 | pip install pdbr[ipython] 153 | ``` 154 | 155 | ## pytest 156 | In order to use `pdbr` with pytest `--pdb` flag, add `addopts` setting in your pytest.ini. 157 | 158 | ``` 159 | [pytest] 160 | addopts: --pdbcls=pdbr:RichPdb 161 | ``` 162 | 163 | ## sys.excepthook 164 | The `sys.excepthook` is a Python system hook that provides a way to customize the behavior when an unhandled exception occurs. Since `pdbr` use automatic traceback handler feature of `rich`, formatting exception print is not necessary if `pdbr` module is already imported. 165 | 166 | In order to use post-mortem or perform other debugging features of `pdbr`, override `sys.excepthook` with a function that will act as your custom excepthook: 167 | ```python 168 | import sys 169 | import pdbr 170 | 171 | def custom_excepthook(exc_type, exc_value, exc_traceback): 172 | pdbr.post_mortem(exc_traceback, exc_value) 173 | 174 | # [Optional] call the original excepthook as well 175 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 176 | 177 | sys.excepthook = custom_excepthook 178 | ``` 179 | Now, whenever an unhandled exception occurs, `pdbr` will be triggered, allowing you to debug the issue interactively. 180 | 181 | ## Context Decorator 182 | `pdbr_context` and `apdbr_context` (`asyncio` corresponding) can be used as **with statement** or **decorator**. It calls `post_mortem` if `traceback` is not none. 183 | 184 | ```python 185 | from pdbr import apdbr_context, pdbr_context 186 | 187 | @pdbr_context() 188 | def foo(): 189 | ... 190 | 191 | def bar(): 192 | with pdbr_context(): 193 | ... 194 | 195 | 196 | @apdbr_context() 197 | async def foo(): 198 | ... 199 | 200 | async def bar(): 201 | async with apdbr_context(): 202 | ... 203 | ``` 204 | 205 | ![](/images/image12.png) 206 | ## Django DiscoverRunner 207 | To being activated the pdb in Django test, change `TEST_RUNNER` like below. Unlike Django (since you are not allowed to use for smaller versions than 3), pdbr runner can be used for version 1.8 and subsequent versions. 208 | 209 | ``` 210 | TEST_RUNNER = "pdbr.runner.PdbrDiscoverRunner" 211 | ``` 212 | ![](/images/image10.png) 213 | ## Middlewares 214 | ### Starlette 215 | ```python 216 | from fastapi import FastAPI 217 | from pdbr.middlewares.starlette import PdbrMiddleware 218 | 219 | app = FastAPI() 220 | 221 | app.add_middleware(PdbrMiddleware, debug=True) 222 | 223 | 224 | @app.get("/") 225 | async def main(): 226 | 1 / 0 227 | return {"message": "Hello World"} 228 | ``` 229 | ### Django 230 | In order to catch the problematic codes with post mortem, place the middleware class. 231 | 232 | ``` 233 | MIDDLEWARE = ( 234 | ... 235 | "pdbr.middlewares.django.PdbrMiddleware", 236 | ) 237 | ``` 238 | ![](/images/image11.png) 239 | ## Shell 240 | Running `pdbr` command in terminal starts an `IPython` terminal app instance. Unlike default `TerminalInteractiveShell`, the new shell uses pdbr as debugger class instead of `ipdb`. 241 | #### %debug magic sample 242 | ![](/images/image9.png) 243 | ### As a Script 244 | If `pdbr` command is used with an argument, it is invoked as a script and [debugger-commands](https://docs.python.org/3/library/pdb.html#debugger-commands) can be used with it. 245 | ```python 246 | # equivalent code: `python -m pdbr -c 'b 5' my_test.py` 247 | pdbr -c 'b 5' my_test.py 248 | 249 | >>> Breakpoint 1 at /my_test.py:5 250 | > /my_test.py(3)() 251 | 1 252 | 2 253 | ----> 3 def test(): 254 | 4 foo = "foo" 255 | 1 5 bar = "bar" 256 | 257 | (Pdbr) 258 | 259 | ``` 260 | ### Terminal 261 | #### Django shell sample 262 | ![](/images/image7.png) 263 | 264 | ## Vscode user snippet 265 | 266 | To create or edit your own snippets, select **User Snippets** under **File > Preferences** (**Code > Preferences** on macOS), and then select **python.json**. 267 | 268 | Place the below snippet in json file for **pdbr**. 269 | 270 | ``` 271 | { 272 | ... 273 | "pdbr": { 274 | "prefix": "pdbr", 275 | "body": "import pdbr; pdbr.set_trace()", 276 | "description": "Code snippet for pdbr debug" 277 | }, 278 | } 279 | ``` 280 | 281 | For **Celery** debug. 282 | 283 | ``` 284 | { 285 | ... 286 | "rdbr": { 287 | "prefix": "rdbr", 288 | "body": "import pdbr; pdbr.celery_set_trace()", 289 | "description": "Code snippet for Celery pdbr debug" 290 | }, 291 | } 292 | ``` 293 | 294 | ## Samples 295 | ![](/images/image1.png) 296 | 297 | ![](/images/image3.png) 298 | 299 | ![](/images/image4.png) 300 | 301 | ### Traceback 302 | ![](/images/image2.png) 303 | -------------------------------------------------------------------------------- /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image1.png -------------------------------------------------------------------------------- /images/image10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image10.png -------------------------------------------------------------------------------- /images/image11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image11.png -------------------------------------------------------------------------------- /images/image12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image12.png -------------------------------------------------------------------------------- /images/image13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image13.png -------------------------------------------------------------------------------- /images/image14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image14.png -------------------------------------------------------------------------------- /images/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image2.png -------------------------------------------------------------------------------- /images/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image3.png -------------------------------------------------------------------------------- /images/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image4.png -------------------------------------------------------------------------------- /images/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image5.png -------------------------------------------------------------------------------- /images/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image6.png -------------------------------------------------------------------------------- /images/image7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image7.png -------------------------------------------------------------------------------- /images/image9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/images/image9.png -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | nox.options.stop_on_first_error = True 4 | 5 | 6 | @nox.session 7 | def test(session, reuse_venv=True): 8 | session.install( 9 | ".", 10 | "pytest", 11 | "pytest-cov", 12 | "rich", 13 | "prompt_toolkit", 14 | "IPython", 15 | ) 16 | session.run( 17 | "pytest", 18 | "--cov-report", 19 | "term-missing", 20 | "--cov=pdbr", 21 | "--capture=no", 22 | "--disable-warnings", 23 | "tests", 24 | ) 25 | 26 | 27 | @nox.session 28 | @nox.parametrize("django", ["3.2", "4.2"]) 29 | def django_test(session, django, reuse_venv=True): 30 | session.install(f"django=={django}", "rich", "pytest") 31 | session.run("python", "runtests.py") 32 | -------------------------------------------------------------------------------- /pdbr/__init__.py: -------------------------------------------------------------------------------- 1 | from pdbr.__main__ import RichPdb, celery_set_trace, pm, post_mortem, run, set_trace 2 | from pdbr._cm import apdbr_context, pdbr_context 3 | 4 | __all__ = [ 5 | "set_trace", 6 | "run", 7 | "pm", 8 | "post_mortem", 9 | "celery_set_trace", 10 | "RichPdb", 11 | "pdbr_context", 12 | "apdbr_context", 13 | ] 14 | -------------------------------------------------------------------------------- /pdbr/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pdb 3 | import sys 4 | 5 | from .utils import _pdbr_cls, _rdbr_cls 6 | 7 | os.environ["PYTHONBREAKPOINT"] = "pdbr.set_trace" 8 | 9 | RichPdb = _pdbr_cls(return_instance=False, show_layouts=False) 10 | 11 | 12 | def set_trace(*, console=None, header=None, context=None, show_layouts=False): 13 | pdb_cls = _pdbr_cls(console=console, context=context, show_layouts=show_layouts) 14 | if header is not None: 15 | pdb_cls.message(header) 16 | pdb_cls.set_trace(sys._getframe().f_back) 17 | 18 | 19 | def run(statement, globals=None, locals=None): 20 | RichPdb().run(statement, globals, locals) 21 | 22 | 23 | def post_mortem(traceback=None, value=None): 24 | _, sys_value, sys_traceback = sys.exc_info() 25 | value = value or sys_value 26 | traceback = traceback or sys_traceback 27 | 28 | if traceback is None: 29 | raise ValueError( 30 | "A valid traceback must be passed if no exception is being handled" 31 | ) 32 | 33 | p = RichPdb() 34 | p.reset() 35 | if value: 36 | p.error(value) 37 | p.interaction(None, traceback) 38 | 39 | 40 | def pm(): 41 | post_mortem(sys.last_traceback) 42 | 43 | 44 | def celery_set_trace(frame=None): 45 | pdb_cls = _rdbr_cls() 46 | if frame is None: 47 | frame = sys._getframe().f_back 48 | return pdb_cls.set_trace(frame) 49 | 50 | 51 | def main(): 52 | pdb.Pdb = RichPdb 53 | pdb.main() 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /pdbr/_cm.py: -------------------------------------------------------------------------------- 1 | from contextlib import ContextDecorator 2 | from functools import wraps 3 | 4 | from pdbr.__main__ import post_mortem 5 | 6 | 7 | class pdbr_context(ContextDecorator): 8 | def __init__(self, suppress_exc=True, debug=True): 9 | self.suppress_exc = suppress_exc 10 | self.debug = debug 11 | 12 | def __enter__(self): 13 | return self 14 | 15 | def __exit__(self, _, exc_value, exc_traceback): 16 | if exc_traceback and self.debug: 17 | post_mortem(exc_traceback, exc_value) 18 | return self.suppress_exc 19 | return False 20 | 21 | 22 | class AsyncContextDecorator(ContextDecorator): 23 | def __call__(self, func): 24 | @wraps(func) 25 | async def inner(*args, **kwds): 26 | async with self._recreate_cm(): 27 | return await func(*args, **kwds) 28 | 29 | return inner 30 | 31 | 32 | class apdbr_context(AsyncContextDecorator): 33 | def __init__(self, suppress_exc=True, debug=True): 34 | self.suppress_exc = suppress_exc 35 | self.debug = debug 36 | 37 | async def __aenter__(self): 38 | return self 39 | 40 | async def __aexit__(self, _, exc_value, exc_traceback): 41 | if exc_traceback and self.debug: 42 | post_mortem(exc_traceback, exc_value) 43 | return self.suppress_exc 44 | return False 45 | -------------------------------------------------------------------------------- /pdbr/_console_layout.py: -------------------------------------------------------------------------------- 1 | from rich.containers import Lines 2 | from rich.errors import NotRenderableError 3 | from rich.layout import Layout 4 | from rich.panel import Panel 5 | 6 | 7 | class ConsoleLayoutMeta(type): 8 | _instances = {} 9 | 10 | def __call__(cls, *args, **kwargs): 11 | if cls not in cls._instances: 12 | instance = super().__call__(*args, **kwargs) 13 | cls._instances[cls] = instance 14 | return cls._instances[cls] 15 | 16 | 17 | class ConsoleLayout(metaclass=ConsoleLayoutMeta): 18 | def __init__(self, console): 19 | self.console = console 20 | self.layout = self._prep_layout() 21 | 22 | def _prep_layout(self): 23 | layout = Layout() 24 | right_body = Layout(name="right_body", ratio=1) 25 | 26 | layout.split( 27 | Layout(name="left_body", ratio=2), 28 | right_body, 29 | splitter="row", 30 | ) 31 | 32 | right_body.split( 33 | Layout(name="up_footer", ratio=2), Layout(name="bottom_footer", ratio=1) 34 | ) 35 | return layout 36 | 37 | def print(self, message, code, stack_trace, vars, **kwargs): 38 | try: 39 | self.layout["left_body"].update(code) 40 | self.layout["up_footer"].update(Panel(vars, title="Locals")) 41 | 42 | self.layout["bottom_footer"].update( 43 | Panel(Lines(stack_trace), title="Stack", style="white on blue") 44 | ) 45 | 46 | self.console.print(self.layout, **kwargs) 47 | self.console.print(message, **kwargs) 48 | except NotRenderableError: 49 | self.console.print(message, **kwargs) 50 | -------------------------------------------------------------------------------- /pdbr/_pdbr.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import io 3 | import re 4 | from pathlib import Path 5 | from pdb import Pdb 6 | 7 | from rich import box, markup 8 | from rich._inspect import Inspect 9 | from rich.console import Console 10 | from rich.panel import Panel 11 | from rich.pretty import pprint 12 | from rich.syntax import DEFAULT_THEME, Syntax 13 | from rich.table import Table 14 | from rich.text import Text 15 | from rich.theme import Theme 16 | from rich.tree import Tree 17 | 18 | from pdbr._console_layout import ConsoleLayout 19 | 20 | try: 21 | from IPython.terminal.interactiveshell import TerminalInteractiveShell 22 | 23 | TerminalInteractiveShell.simple_prompt = False 24 | except ImportError: 25 | pass 26 | 27 | WITHOUT_LAYOUT_COMMANDS = ( 28 | "where", 29 | "w", 30 | ) 31 | ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") 32 | 33 | 34 | class AsciiStdout(io.TextIOWrapper): 35 | pass 36 | 37 | 38 | def rich_pdb_klass( 39 | base, is_celery=False, console=None, context=None, show_layouts=True 40 | ): 41 | class RichPdb(base): 42 | _style = None 43 | _theme = None 44 | _history_file = None 45 | _ipython_history_file = None 46 | _latest_search_arg = "" 47 | 48 | def __init__( 49 | self, 50 | completekey="tab", 51 | stdin=None, 52 | stdout=None, 53 | skip=None, 54 | nosigint=False, 55 | readrc=True, 56 | ): 57 | init_kwargs = ( 58 | {"out": stdout} 59 | if is_celery 60 | else { 61 | "completekey": completekey, 62 | "stdin": stdin, 63 | "stdout": stdout, 64 | "skip": skip, 65 | "nosigint": nosigint, 66 | "readrc": readrc, 67 | } 68 | ) 69 | if console is not None: 70 | self._console = console 71 | if context is not None: 72 | if base == Pdb: 73 | raise ValueError("Context can only be used with IPython") 74 | init_kwargs["context"] = context 75 | super().__init__(**init_kwargs) 76 | 77 | self.prompt = "(Pdbr) " 78 | 79 | def pt_init(self, pt_session_options=None): 80 | from prompt_toolkit.history import FileHistory 81 | 82 | if self._ipython_history_file: 83 | history_file = FileHistory(self._ipython_history_file) 84 | self.shell.debugger_history = history_file 85 | # In order to fix the error for ipython 8.x 86 | self.debugger_history = history_file 87 | 88 | func = super().pt_init 89 | func_args = inspect.getfullargspec(super().pt_init).args 90 | if "pt_session_options" in func_args: 91 | func(pt_session_options) 92 | else: 93 | func() 94 | 95 | @property 96 | def console(self): 97 | if not hasattr(self, "_console"): 98 | self._console = Console( 99 | file=( 100 | AsciiStdout(buffer=self.stdout.buffer, encoding="ascii") 101 | if is_celery 102 | else self.stdout 103 | ), 104 | theme=Theme( 105 | {"info": "dim cyan", "warning": "magenta", "danger": "bold red"} 106 | ), 107 | style=self._style, 108 | force_terminal=True, 109 | force_interactive=True, 110 | ) 111 | return self._console 112 | 113 | def do_help(self, arg): 114 | super().do_help(arg) 115 | if not arg: 116 | self._print( 117 | Panel( 118 | "Visit " 119 | "[bold][link=https://github.com/cansarigol/pdbr]" 120 | "https://github.com/cansarigol/pdbr[/link][/]" 121 | " for more!" 122 | ), 123 | style="warning", 124 | print_layout=False, 125 | ) 126 | 127 | do_help.__doc__ = base.do_help.__doc__ 128 | do_h = do_help 129 | 130 | def _get_syntax_for_list(self, line_range=None): 131 | if not line_range: 132 | first = max(1, self.curframe.f_lineno - 5) 133 | line_range = first, first + 10 134 | filename = self.curframe.f_code.co_filename 135 | highlight_lines = {self.curframe.f_lineno} 136 | 137 | return Syntax.from_path( 138 | filename, 139 | line_numbers=True, 140 | theme=self._theme or DEFAULT_THEME, 141 | line_range=line_range, 142 | highlight_lines=highlight_lines, 143 | ) 144 | 145 | def _get_variables(self): 146 | try: 147 | return [ 148 | (k, str(v), str(type(v))) 149 | for k, v in self.curframe.f_locals.items() 150 | if not k.startswith("__") and k != "pdbr" 151 | ] 152 | except AttributeError: 153 | return [] 154 | 155 | def do_l(self, arg): 156 | """l 157 | List 11 lines source code for the current file. 158 | """ 159 | try: 160 | self._print(self._get_syntax_for_list(), print_layout=False) 161 | except BaseException: 162 | self.error("could not get source code") 163 | 164 | def do_longlist(self, arg): 165 | """longlist | ll 166 | List the whole source code for the current function or frame. 167 | """ 168 | try: 169 | lines, lineno = self._getsourcelines(self.curframe) 170 | last = lineno + len(lines) 171 | self._print( 172 | self._get_syntax_for_list((lineno, last)), print_layout=False 173 | ) 174 | except BaseException: 175 | self.error("could not get source code") 176 | 177 | do_ll = do_longlist 178 | 179 | def do_source(self, arg): 180 | """source expression 181 | Try to get source code for the given object and display it. 182 | """ 183 | try: 184 | obj = self._getval(arg) 185 | lines, lineno = self._getsourcelines(obj) 186 | last = lineno + len(lines) 187 | self._print( 188 | self._get_syntax_for_list((lineno, last)), print_layout=False 189 | ) 190 | except BaseException as err: 191 | self.error(err) 192 | 193 | def do_search(self, arg): 194 | """search | src 195 | Search a phrase in the current frame. 196 | In order to repeat the last one, type `/` character as arg. 197 | """ 198 | if not arg or (arg == "/" and not self._latest_search_arg): 199 | self.error("Search failed: arg is missing") 200 | return 201 | 202 | if arg == "/": 203 | arg = self._latest_search_arg 204 | else: 205 | self._latest_search_arg = arg 206 | 207 | lines, lineno = self._getsourcelines(self.curframe) 208 | indexes = [index for index, line in enumerate(lines, lineno) if arg in line] 209 | 210 | if len(indexes) > 0: 211 | bigger_indexes = [ 212 | index for index in indexes if index > self.curframe.f_lineno 213 | ] 214 | next_line = bigger_indexes[0] if bigger_indexes else indexes[0] 215 | return super().do_jump(next_line) 216 | else: 217 | self.error(f"Search failed: '{arg}' not found") 218 | 219 | do_src = do_search 220 | 221 | def _getsourcelines(self, obj): 222 | lines, lineno = inspect.getsourcelines(obj) 223 | lineno = max(1, lineno) 224 | return lines, lineno 225 | 226 | def get_varstable(self): 227 | variables = self._get_variables() 228 | if not variables: 229 | return 230 | table = Table(title="List of local variables", box=box.MINIMAL) 231 | 232 | table.add_column("Variable", style="cyan") 233 | table.add_column("Value", style="magenta") 234 | table.add_column("Type", style="green") 235 | [ 236 | table.add_row(variable, value, _type) 237 | for variable, value, _type in variables 238 | ] 239 | return table 240 | 241 | def do_v(self, arg): 242 | """v(ars) 243 | List of local variables 244 | """ 245 | self._print(self.get_varstable(), print_layout=False) 246 | 247 | def get_varstree(self): 248 | variables = self._get_variables() 249 | if not variables: 250 | return 251 | tree_key = "" 252 | type_tree = None 253 | tree = Tree("Variables") 254 | 255 | for variable, value, _type in sorted( 256 | variables, key=lambda item: (item[2], item[0]) 257 | ): 258 | if tree_key != _type: 259 | if tree_key != "": 260 | tree.add(type_tree, style="bold green") 261 | type_tree = Tree(_type) 262 | tree_key = _type 263 | type_tree.add(f"{variable}: {value}", style="magenta") 264 | if type_tree: 265 | tree.add(type_tree, style="bold green") 266 | return tree 267 | 268 | def do_varstree(self, arg): 269 | """varstree | vt 270 | List of local variables in Rich.Tree 271 | """ 272 | self._print(self.get_varstree(), print_layout=False) 273 | 274 | do_vt = do_varstree 275 | 276 | def do_inspect(self, arg, all=False): 277 | """(i)nspect 278 | Display the data / methods / docs for any Python object. 279 | """ 280 | try: 281 | self._print( 282 | Inspect(self._getval(arg), methods=True, all=all), 283 | print_layout=False, 284 | ) 285 | except BaseException: 286 | pass 287 | 288 | def do_inspectall(self, arg): 289 | """inspectall | ia 290 | Inspect with all to see all attributes. 291 | """ 292 | self.do_inspect(arg, all=True) 293 | 294 | do_i = do_inspect 295 | do_ia = do_inspectall 296 | 297 | def do_pp(self, arg): 298 | """pp expression 299 | Rich pretty print. 300 | """ 301 | try: 302 | pprint(self._getval(arg), console=self.console) 303 | except BaseException: 304 | pass 305 | 306 | def do_syntax(self, arg): 307 | """syn(tax)[ val,lexer ] 308 | Display lexer. https://pygments.org/docs/lexers/ 309 | """ 310 | try: 311 | val, lexer = arg.split(",") 312 | val = val.strip() 313 | lexer = lexer.strip() 314 | val = Syntax( 315 | self._getval(val), 316 | self._getval(lexer), 317 | theme=self._theme or DEFAULT_THEME, 318 | ) 319 | self._print(val) 320 | except BaseException: 321 | pass 322 | 323 | do_syn = do_syntax 324 | 325 | def do_sql(self, arg): 326 | """sql 327 | Display value in sql format. 328 | """ 329 | try: 330 | import sqlparse 331 | 332 | val = sqlparse.format( 333 | self._getval(arg), reindent=True, keyword_case="upper" 334 | ) 335 | self._print(val) 336 | except ModuleNotFoundError as error: 337 | raise type(error)("Install sqlparse to see sql format.") from error 338 | 339 | def displayhook(self, obj): 340 | if obj is not None: 341 | self._print(obj if isinstance(obj, (dict, list)) else repr(obj)) 342 | 343 | def error(self, msg): 344 | self._print(msg, prefix="***", style="danger", print_layout=False) 345 | 346 | def _format_stack_entry(self, frame_lineno): 347 | stack_entry = Pdb.format_stack_entry(self, frame_lineno, "\n") 348 | return stack_entry.replace(str(Path.cwd().absolute()), "") 349 | 350 | def stack_trace(self): 351 | stacks = [] 352 | try: 353 | for frame_lineno in self.stack: 354 | frame, _ = frame_lineno 355 | if frame is self.curframe: 356 | prefix = "-> " 357 | else: 358 | prefix = " " 359 | 360 | stack_entry = self._format_stack_entry(frame_lineno) 361 | first_line, _ = stack_entry.splitlines() 362 | text_body = Text(stack_entry) 363 | text_prefix = Text(prefix) 364 | text_body.stylize("bold", len(first_line), len(stack_entry)) 365 | text_prefix.stylize("bold") 366 | stacks.append(Text.assemble(text_prefix, text_body)) 367 | except KeyboardInterrupt: 368 | pass 369 | return reversed(stacks) 370 | 371 | def message(self, msg): 372 | "this is used by the upstream PDB class" 373 | self._print(msg) 374 | 375 | def precmd(self, line): 376 | if line.endswith("??"): 377 | line = "pinfo2 " + line[:-2] 378 | elif line.endswith("?"): 379 | line = "pinfo " + line[:-1] 380 | 381 | return super().precmd(line) 382 | 383 | def onecmd(self, line: str) -> bool: 384 | """ 385 | Invokes 'run_magic()' if the line starts with a '%'. 386 | The loop stops of this function returns True. 387 | (unless an overridden 'postcmd()' behaves differently) 388 | """ 389 | try: 390 | line = line.strip() 391 | if line.startswith("%"): 392 | if line.startswith("%%"): 393 | self.error( 394 | "Cell magics (multiline) are not yet supported. " 395 | "Use a single '%' instead." 396 | ) 397 | return False 398 | self.run_magic(line[1:]) 399 | return False 400 | return super().onecmd(line) 401 | 402 | except Exception as e: 403 | self.error(f"{type(e).__qualname__} in onecmd({line!r}): {e}") 404 | return False 405 | 406 | def _print(self, val, prefix=None, style=None, print_layout=True): 407 | if val == "--Return--": 408 | return 409 | 410 | if isinstance(val, str) and ("[0m" in val or "[/" in val): 411 | val = markup.render(val) 412 | 413 | kwargs = {"style": str(style)} if style else {} 414 | args = (prefix, val) if prefix else (val,) 415 | if ( 416 | show_layouts 417 | and print_layout 418 | and self.lastcmd not in WITHOUT_LAYOUT_COMMANDS 419 | ): 420 | self._print_layout(*args, **kwargs) 421 | else: 422 | self.console.print(*args, **kwargs) 423 | 424 | def _print_layout(self, val, **kwargs): 425 | ConsoleLayout(self.console).print( 426 | val, 427 | code=self._get_syntax_for_list(), 428 | stack_trace=self.stack_trace(**kwargs), 429 | vars=self.get_varstree(), 430 | **kwargs, 431 | ) 432 | 433 | def print_stack_entry(self, frame_lineno, prompt_prefix="\n-> "): 434 | def print_syntax(*args): 435 | # Remove color format. 436 | self._print( 437 | Syntax( 438 | ANSI_ESCAPE.sub("", self.format_stack_entry(*args)), 439 | "python", 440 | theme=self._theme or DEFAULT_THEME, 441 | ), 442 | print_layout=False, 443 | ) 444 | 445 | if is_celery: 446 | Pdb.print_stack_entry(self, frame_lineno, prompt_prefix) 447 | elif base == Pdb: 448 | print_syntax(frame_lineno, prompt_prefix) 449 | else: 450 | print_syntax(frame_lineno, "") 451 | 452 | # vds: >> 453 | frame, lineno = frame_lineno 454 | filename = frame.f_code.co_filename 455 | self.shell.hooks.synchronize_with_editor(filename, lineno, 0) 456 | # vds: << 457 | 458 | def run_magic(self, line) -> str: 459 | """ 460 | Parses the line and runs the appropriate magic function. 461 | Assumes that the line is without a leading '%'. 462 | """ 463 | magic_name, arg, line = self.parseline(line) 464 | if hasattr(self, f"do_{magic_name}"): 465 | # We want to use do_{magic_name} methods if defined. 466 | # This is indeed the case with do_pdef, do_pdoc etc, 467 | # which are defined by our base class (IPython.core.debugger.Pdb). 468 | result = getattr(self, f"do_{magic_name}")(arg) 469 | else: 470 | magic_fn = self.shell.find_line_magic(magic_name) 471 | if not magic_fn: 472 | self.error(f"Line Magic %{magic_name} not found") 473 | return "" 474 | if magic_name in ("time", "timeit"): 475 | result = magic_fn( 476 | arg, 477 | local_ns={**self.curframe_locals, **self.curframe.f_globals}, 478 | ) 479 | else: 480 | result = magic_fn(arg) 481 | if result: 482 | result = str(result) 483 | self._print(result) 484 | return "" 485 | 486 | return RichPdb 487 | -------------------------------------------------------------------------------- /pdbr/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from telnetlib import Telnet 3 | 4 | from rich.file_proxy import FileProxy 5 | 6 | from pdbr.helpers import run_ipython_shell 7 | 8 | 9 | def shell(): 10 | import getopt 11 | 12 | _, args = getopt.getopt(sys.argv[1:], "mhc:", ["command="]) 13 | 14 | if not args: 15 | run_ipython_shell() 16 | else: 17 | from pdbr.__main__ import main 18 | 19 | main() 20 | 21 | 22 | def telnet(): 23 | from pdbr.__main__ import RichPdb 24 | 25 | pdb_cls = RichPdb() 26 | if len(sys.argv) < 3: 27 | pdb_cls.error("Usage : pdbr_telnet hostname port") 28 | sys.exit() 29 | 30 | class MyTelnet(Telnet): 31 | def fill_rawq(self): 32 | """ 33 | exactly the same with Telnet.fill_rawq, 34 | buffer size is just changed from 50 to 1024. 35 | """ 36 | if self.irawq >= len(self.rawq): 37 | self.rawq = b"" 38 | self.irawq = 0 39 | buf = self.sock.recv(1024) 40 | self.msg("recv %r", buf) 41 | self.eof = not buf 42 | self.rawq = self.rawq + buf 43 | 44 | console = pdb_cls.console 45 | sys.stdout = FileProxy(console, sys.stdout) 46 | sys.stderr = FileProxy(console, sys.stderr) 47 | try: 48 | host = sys.argv[1] 49 | port = int(sys.argv[2]) 50 | with MyTelnet(host, port) as tn: 51 | tn.interact() 52 | except BaseException as e: 53 | pdb_cls.error(e) 54 | sys.exit() 55 | -------------------------------------------------------------------------------- /pdbr/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pdbr.__main__ import RichPdb 4 | 5 | 6 | def run_ipython_shell(): 7 | try: 8 | from IPython.terminal.interactiveshell import TerminalInteractiveShell 9 | from IPython.terminal.ipapp import TerminalIPythonApp 10 | from prompt_toolkit.history import FileHistory 11 | from traitlets import Type 12 | 13 | TerminalInteractiveShell.simple_prompt = False 14 | except ModuleNotFoundError as error: 15 | raise type(error)( 16 | "In order to use pdbr shell, install IPython with pdbr[ipython]" 17 | ) from error 18 | 19 | class PdbrTerminalInteractiveShell(TerminalInteractiveShell): 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | if RichPdb._ipython_history_file: 24 | self.debugger_history = FileHistory(RichPdb._ipython_history_file) 25 | 26 | @property 27 | def debugger_cls(self): 28 | return RichPdb 29 | 30 | class PdbrTerminalIPythonApp(TerminalIPythonApp): 31 | interactive_shell_class = Type( 32 | klass=object, # use default_value otherwise which only allow subclasses. 33 | default_value=PdbrTerminalInteractiveShell, 34 | help=( 35 | "Class to use to instantiate the TerminalInteractiveShell object. " 36 | "Useful for custom Frontends" 37 | ), 38 | ).tag(config=True) 39 | 40 | app = PdbrTerminalIPythonApp.instance() 41 | app.initialize() 42 | sys.exit(app.start()) 43 | -------------------------------------------------------------------------------- /pdbr/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/pdbr/middlewares/__init__.py -------------------------------------------------------------------------------- /pdbr/middlewares/django.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.conf import settings 4 | from django.core.exceptions import MiddlewareNotUsed 5 | 6 | from pdbr.__main__ import post_mortem 7 | 8 | 9 | class PdbrMiddleware: 10 | def __init__(self, get_response): 11 | if not settings.DEBUG: 12 | raise MiddlewareNotUsed() 13 | self.get_response = get_response 14 | 15 | def __call__(self, request): 16 | return self.get_response(request) 17 | 18 | def process_exception(self, request, exception): # noqa: F841 19 | post_mortem(sys.exc_info()[2]) 20 | -------------------------------------------------------------------------------- /pdbr/middlewares/starlette.py: -------------------------------------------------------------------------------- 1 | from starlette.middleware.errors import ServerErrorMiddleware 2 | 3 | from pdbr._cm import apdbr_context 4 | 5 | 6 | class PdbrMiddleware(ServerErrorMiddleware): 7 | async def __call__(self, scope, receive, send) -> None: 8 | async with apdbr_context(suppress_exc=False, debug=self.debug): 9 | await super().__call__(scope, receive, send) 10 | -------------------------------------------------------------------------------- /pdbr/runner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.test.runner import DebugSQLTextTestResult, DiscoverRunner 4 | 5 | from pdbr.__main__ import RichPdb, post_mortem 6 | 7 | 8 | class PDBRDebugResult(unittest.TextTestResult): 9 | _pdbr = RichPdb() 10 | 11 | def addError(self, test, err): 12 | super().addError(test, err) 13 | self._print(test, err) 14 | 15 | def addFailure(self, test, err): 16 | super().addFailure(test, err) 17 | self._print(test, err) 18 | 19 | def _print(self, test, err): 20 | self.buffer = False 21 | self._pdbr.message(f"\n{test}") 22 | self._pdbr.error("%s: %s", err[0].__name__, err[1]) 23 | post_mortem(err[2]) 24 | 25 | 26 | class PdbrDiscoverRunner(DiscoverRunner): 27 | def get_resultclass(self): 28 | if self.debug_sql: 29 | return DebugSQLTextTestResult 30 | return PDBRDebugResult 31 | -------------------------------------------------------------------------------- /pdbr/utils.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import configparser 3 | import os 4 | from pathlib import Path 5 | 6 | from pdbr._pdbr import rich_pdb_klass 7 | 8 | try: 9 | import readline 10 | except ImportError: 11 | try: 12 | from pyreadline3 import Readline 13 | 14 | readline = Readline() 15 | except ModuleNotFoundError: 16 | readline = None 17 | except AttributeError: 18 | readline = None 19 | 20 | 21 | def set_history_file(history_file): 22 | """ 23 | This is just for Pdb, 24 | For Ipython, look at RichPdb.pt_init 25 | """ 26 | if readline is None: 27 | return 28 | try: 29 | readline.read_history_file(history_file) 30 | readline.set_history_length(1000) 31 | except FileNotFoundError: 32 | pass 33 | except OSError: 34 | pass 35 | 36 | atexit.register(readline.write_history_file, history_file) 37 | 38 | 39 | def set_traceback(theme): 40 | from rich.traceback import install 41 | 42 | install(theme=theme) 43 | 44 | 45 | def read_config(): 46 | style = None 47 | theme = None 48 | store_history = ".pdbr_history" 49 | context = None 50 | 51 | config = configparser.ConfigParser() 52 | config.sections() 53 | 54 | setup_filename = "setup.cfg" 55 | xdg_config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) 56 | global_config_path = xdg_config_home / "pdbr" / setup_filename 57 | cwd_config_path = Path.cwd() / setup_filename 58 | config_path = cwd_config_path.exists() and cwd_config_path or global_config_path 59 | 60 | config.read(config_path) 61 | if "pdbr" in config: 62 | if "style" in config["pdbr"]: 63 | style = config["pdbr"]["style"] 64 | 65 | if "theme" in config["pdbr"]: 66 | theme = config["pdbr"]["theme"] 67 | 68 | if "use_traceback" in config["pdbr"]: 69 | if config["pdbr"]["use_traceback"].lower() == "true": 70 | set_traceback(theme) 71 | else: 72 | set_traceback(theme) 73 | 74 | if "store_history" in config["pdbr"]: 75 | store_history = config["pdbr"]["store_history"] 76 | 77 | if "context" in config["pdbr"]: 78 | context = config["pdbr"]["context"] 79 | 80 | history_file = str(Path.home() / store_history) 81 | set_history_file(history_file) 82 | ipython_history_file = f"{history_file}_ipython" 83 | 84 | return style, theme, history_file, ipython_history_file, context 85 | 86 | 87 | def debugger_cls( 88 | klass=None, console=None, context=None, is_celery=False, show_layouts=True 89 | ): 90 | if klass is None: 91 | try: 92 | from IPython.terminal.debugger import TerminalPdb 93 | 94 | klass = TerminalPdb 95 | except ImportError: 96 | from pdb import Pdb 97 | 98 | klass = Pdb 99 | 100 | style, theme, history_file, ipython_history_file, config_context = read_config() 101 | RichPdb = rich_pdb_klass( 102 | klass, 103 | console=console, 104 | context=context if context is not None else config_context, 105 | is_celery=is_celery, 106 | show_layouts=show_layouts, 107 | ) 108 | RichPdb._style = style 109 | RichPdb._theme = theme 110 | RichPdb._history_file = history_file 111 | RichPdb._ipython_history_file = ipython_history_file 112 | 113 | return RichPdb 114 | 115 | 116 | def _pdbr_cls(console=None, context=None, return_instance=True, show_layouts=True): 117 | klass = debugger_cls(console=console, context=context, show_layouts=show_layouts) 118 | if return_instance: 119 | return klass() 120 | return klass 121 | 122 | 123 | def _rdbr_cls(return_instance=True): 124 | try: 125 | from celery.contrib import rdb 126 | 127 | rdb.BANNER = """\ 128 | {self.ident}: Type `pdbr_telnet {self.host} {self.port}` to connect 129 | 130 | {self.ident}: Waiting for client... 131 | """ 132 | except ModuleNotFoundError as error: 133 | raise type(error)("In order to install celery, use pdbr[celery]") from error 134 | 135 | klass = debugger_cls(klass=rdb.Rdb, is_celery=True, show_layouts=False) 136 | if return_instance: 137 | return klass() 138 | return klass 139 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appnope" 5 | version = "0.1.3" 6 | description = "Disable App Nap on macOS >= 10.9" 7 | optional = true 8 | python-versions = "*" 9 | files = [ 10 | {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, 11 | {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, 12 | ] 13 | 14 | [[package]] 15 | name = "argcomplete" 16 | version = "1.10.3" 17 | description = "Bash tab completion for argparse" 18 | optional = false 19 | python-versions = "*" 20 | files = [ 21 | {file = "argcomplete-1.10.3-py2.py3-none-any.whl", hash = "sha256:d8ea63ebaec7f59e56e7b2a386b1d1c7f1a7ae87902c9ee17d377eaa557f06fa"}, 22 | {file = "argcomplete-1.10.3.tar.gz", hash = "sha256:a37f522cf3b6a34abddfedb61c4546f60023b3799b22d1cd971eacdc0861530a"}, 23 | ] 24 | 25 | [package.extras] 26 | test = ["coverage", "flake8", "pexpect", "wheel"] 27 | 28 | [[package]] 29 | name = "backcall" 30 | version = "0.2.0" 31 | description = "Specifications for callback functions passed in to an API" 32 | optional = true 33 | python-versions = "*" 34 | files = [ 35 | {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, 36 | {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, 37 | ] 38 | 39 | [[package]] 40 | name = "colorama" 41 | version = "0.4.6" 42 | description = "Cross-platform colored terminal text." 43 | optional = false 44 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 45 | files = [ 46 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 47 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 48 | ] 49 | 50 | [[package]] 51 | name = "colorlog" 52 | version = "6.7.0" 53 | description = "Add colours to the output of Python's logging module." 54 | optional = false 55 | python-versions = ">=3.6" 56 | files = [ 57 | {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, 58 | {file = "colorlog-6.7.0.tar.gz", hash = "sha256:bd94bd21c1e13fac7bd3153f4bc3a7dc0eb0974b8bc2fdf1a989e474f6e582e5"}, 59 | ] 60 | 61 | [package.dependencies] 62 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 63 | 64 | [package.extras] 65 | development = ["black", "flake8", "mypy", "pytest", "types-colorama"] 66 | 67 | [[package]] 68 | name = "decorator" 69 | version = "5.1.1" 70 | description = "Decorators for Humans" 71 | optional = true 72 | python-versions = ">=3.5" 73 | files = [ 74 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 75 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 76 | ] 77 | 78 | [[package]] 79 | name = "distlib" 80 | version = "0.3.7" 81 | description = "Distribution utilities" 82 | optional = false 83 | python-versions = "*" 84 | files = [ 85 | {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, 86 | {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, 87 | ] 88 | 89 | [[package]] 90 | name = "filelock" 91 | version = "3.12.2" 92 | description = "A platform independent file lock." 93 | optional = false 94 | python-versions = ">=3.7" 95 | files = [ 96 | {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, 97 | {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, 98 | ] 99 | 100 | [package.extras] 101 | docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] 102 | testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] 103 | 104 | [[package]] 105 | name = "importlib-metadata" 106 | version = "6.7.0" 107 | description = "Read metadata from Python packages" 108 | optional = false 109 | python-versions = ">=3.7" 110 | files = [ 111 | {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"}, 112 | {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"}, 113 | ] 114 | 115 | [package.dependencies] 116 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 117 | zipp = ">=0.5" 118 | 119 | [package.extras] 120 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 121 | perf = ["ipython"] 122 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] 123 | 124 | [[package]] 125 | name = "ipython" 126 | version = "7.34.0" 127 | description = "IPython: Productive Interactive Computing" 128 | optional = true 129 | python-versions = ">=3.7" 130 | files = [ 131 | {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"}, 132 | {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"}, 133 | ] 134 | 135 | [package.dependencies] 136 | appnope = {version = "*", markers = "sys_platform == \"darwin\""} 137 | backcall = "*" 138 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 139 | decorator = "*" 140 | jedi = ">=0.16" 141 | matplotlib-inline = "*" 142 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} 143 | pickleshare = "*" 144 | prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" 145 | pygments = "*" 146 | setuptools = ">=18.5" 147 | traitlets = ">=4.2" 148 | 149 | [package.extras] 150 | all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] 151 | doc = ["Sphinx (>=1.3)"] 152 | kernel = ["ipykernel"] 153 | nbconvert = ["nbconvert"] 154 | nbformat = ["nbformat"] 155 | notebook = ["ipywidgets", "notebook"] 156 | parallel = ["ipyparallel"] 157 | qtconsole = ["qtconsole"] 158 | test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"] 159 | 160 | [[package]] 161 | name = "jedi" 162 | version = "0.19.1" 163 | description = "An autocompletion tool for Python that can be used for text editors." 164 | optional = true 165 | python-versions = ">=3.6" 166 | files = [ 167 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 168 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 169 | ] 170 | 171 | [package.dependencies] 172 | parso = ">=0.8.3,<0.9.0" 173 | 174 | [package.extras] 175 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 176 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 177 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 178 | 179 | [[package]] 180 | name = "markdown-it-py" 181 | version = "2.2.0" 182 | description = "Python port of markdown-it. Markdown parsing, done right!" 183 | optional = false 184 | python-versions = ">=3.7" 185 | files = [ 186 | {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, 187 | {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, 188 | ] 189 | 190 | [package.dependencies] 191 | mdurl = ">=0.1,<1.0" 192 | typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 193 | 194 | [package.extras] 195 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 196 | code-style = ["pre-commit (>=3.0,<4.0)"] 197 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 198 | linkify = ["linkify-it-py (>=1,<3)"] 199 | plugins = ["mdit-py-plugins"] 200 | profiling = ["gprof2dot"] 201 | rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 202 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 203 | 204 | [[package]] 205 | name = "matplotlib-inline" 206 | version = "0.1.6" 207 | description = "Inline Matplotlib backend for Jupyter" 208 | optional = true 209 | python-versions = ">=3.5" 210 | files = [ 211 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 212 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 213 | ] 214 | 215 | [package.dependencies] 216 | traitlets = "*" 217 | 218 | [[package]] 219 | name = "mdurl" 220 | version = "0.1.2" 221 | description = "Markdown URL utilities" 222 | optional = false 223 | python-versions = ">=3.7" 224 | files = [ 225 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 226 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 227 | ] 228 | 229 | [[package]] 230 | name = "nox" 231 | version = "2024.4.15" 232 | description = "Flexible test automation." 233 | optional = false 234 | python-versions = ">=3.7" 235 | files = [ 236 | {file = "nox-2024.4.15-py3-none-any.whl", hash = "sha256:6492236efa15a460ecb98e7b67562a28b70da006ab0be164e8821177577c0565"}, 237 | {file = "nox-2024.4.15.tar.gz", hash = "sha256:ecf6700199cdfa9e5ea0a41ff5e6ef4641d09508eda6edb89d9987864115817f"}, 238 | ] 239 | 240 | [package.dependencies] 241 | argcomplete = ">=1.9.4,<4.0" 242 | colorlog = ">=2.6.1,<7.0.0" 243 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 244 | packaging = ">=20.9" 245 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 246 | typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} 247 | virtualenv = ">=20.14.1" 248 | 249 | [package.extras] 250 | tox-to-nox = ["jinja2", "tox"] 251 | uv = ["uv (>=0.1.6)"] 252 | 253 | [[package]] 254 | name = "packaging" 255 | version = "23.2" 256 | description = "Core utilities for Python packages" 257 | optional = false 258 | python-versions = ">=3.7" 259 | files = [ 260 | {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, 261 | {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, 262 | ] 263 | 264 | [[package]] 265 | name = "parso" 266 | version = "0.8.3" 267 | description = "A Python Parser" 268 | optional = true 269 | python-versions = ">=3.6" 270 | files = [ 271 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 272 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 273 | ] 274 | 275 | [package.extras] 276 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 277 | testing = ["docopt", "pytest (<6.0.0)"] 278 | 279 | [[package]] 280 | name = "pexpect" 281 | version = "4.8.0" 282 | description = "Pexpect allows easy control of interactive console applications." 283 | optional = true 284 | python-versions = "*" 285 | files = [ 286 | {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, 287 | {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, 288 | ] 289 | 290 | [package.dependencies] 291 | ptyprocess = ">=0.5" 292 | 293 | [[package]] 294 | name = "pickleshare" 295 | version = "0.7.5" 296 | description = "Tiny 'shelve'-like database with concurrency support" 297 | optional = true 298 | python-versions = "*" 299 | files = [ 300 | {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, 301 | {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, 302 | ] 303 | 304 | [[package]] 305 | name = "platformdirs" 306 | version = "3.11.0" 307 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 308 | optional = false 309 | python-versions = ">=3.7" 310 | files = [ 311 | {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, 312 | {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, 313 | ] 314 | 315 | [package.dependencies] 316 | typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""} 317 | 318 | [package.extras] 319 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] 320 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] 321 | 322 | [[package]] 323 | name = "prompt-toolkit" 324 | version = "3.0.39" 325 | description = "Library for building powerful interactive command lines in Python" 326 | optional = true 327 | python-versions = ">=3.7.0" 328 | files = [ 329 | {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, 330 | {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, 331 | ] 332 | 333 | [package.dependencies] 334 | wcwidth = "*" 335 | 336 | [[package]] 337 | name = "ptyprocess" 338 | version = "0.7.0" 339 | description = "Run a subprocess in a pseudo terminal" 340 | optional = true 341 | python-versions = "*" 342 | files = [ 343 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 344 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 345 | ] 346 | 347 | [[package]] 348 | name = "pygments" 349 | version = "2.16.1" 350 | description = "Pygments is a syntax highlighting package written in Python." 351 | optional = false 352 | python-versions = ">=3.7" 353 | files = [ 354 | {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, 355 | {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, 356 | ] 357 | 358 | [package.extras] 359 | plugins = ["importlib-metadata"] 360 | 361 | [[package]] 362 | name = "pyreadline3" 363 | version = "3.4.1" 364 | description = "A python implementation of GNU readline." 365 | optional = false 366 | python-versions = "*" 367 | files = [ 368 | {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, 369 | {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, 370 | ] 371 | 372 | [[package]] 373 | name = "rich" 374 | version = "13.6.0" 375 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 376 | optional = false 377 | python-versions = ">=3.7.0" 378 | files = [ 379 | {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, 380 | {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, 381 | ] 382 | 383 | [package.dependencies] 384 | markdown-it-py = ">=2.2.0" 385 | pygments = ">=2.13.0,<3.0.0" 386 | typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} 387 | 388 | [package.extras] 389 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 390 | 391 | [[package]] 392 | name = "ruff" 393 | version = "0.6.5" 394 | description = "An extremely fast Python linter and code formatter, written in Rust." 395 | optional = false 396 | python-versions = ">=3.7" 397 | files = [ 398 | {file = "ruff-0.6.5-py3-none-linux_armv6l.whl", hash = "sha256:7e4e308f16e07c95fc7753fc1aaac690a323b2bb9f4ec5e844a97bb7fbebd748"}, 399 | {file = "ruff-0.6.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:932cd69eefe4daf8c7d92bd6689f7e8182571cb934ea720af218929da7bd7d69"}, 400 | {file = "ruff-0.6.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a8d42d11fff8d3143ff4da41742a98f8f233bf8890e9fe23077826818f8d680"}, 401 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a50af6e828ee692fb10ff2dfe53f05caecf077f4210fae9677e06a808275754f"}, 402 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:794ada3400a0d0b89e3015f1a7e01f4c97320ac665b7bc3ade24b50b54cb2972"}, 403 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:381413ec47f71ce1d1c614f7779d88886f406f1fd53d289c77e4e533dc6ea200"}, 404 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52e75a82bbc9b42e63c08d22ad0ac525117e72aee9729a069d7c4f235fc4d276"}, 405 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09c72a833fd3551135ceddcba5ebdb68ff89225d30758027280968c9acdc7810"}, 406 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:800c50371bdcb99b3c1551d5691e14d16d6f07063a518770254227f7f6e8c178"}, 407 | {file = "ruff-0.6.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e25ddd9cd63ba1f3bd51c1f09903904a6adf8429df34f17d728a8fa11174253"}, 408 | {file = "ruff-0.6.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7291e64d7129f24d1b0c947ec3ec4c0076e958d1475c61202497c6aced35dd19"}, 409 | {file = "ruff-0.6.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9ad7dfbd138d09d9a7e6931e6a7e797651ce29becd688be8a0d4d5f8177b4b0c"}, 410 | {file = "ruff-0.6.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:005256d977021790cc52aa23d78f06bb5090dc0bfbd42de46d49c201533982ae"}, 411 | {file = "ruff-0.6.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:482c1e6bfeb615eafc5899127b805d28e387bd87db38b2c0c41d271f5e58d8cc"}, 412 | {file = "ruff-0.6.5-py3-none-win32.whl", hash = "sha256:cf4d3fa53644137f6a4a27a2b397381d16454a1566ae5335855c187fbf67e4f5"}, 413 | {file = "ruff-0.6.5-py3-none-win_amd64.whl", hash = "sha256:3e42a57b58e3612051a636bc1ac4e6b838679530235520e8f095f7c44f706ff9"}, 414 | {file = "ruff-0.6.5-py3-none-win_arm64.whl", hash = "sha256:51935067740773afdf97493ba9b8231279e9beef0f2a8079188c4776c25688e0"}, 415 | {file = "ruff-0.6.5.tar.gz", hash = "sha256:4d32d87fab433c0cf285c3683dd4dae63be05fd7a1d65b3f5bf7cdd05a6b96fb"}, 416 | ] 417 | 418 | [[package]] 419 | name = "setuptools" 420 | version = "68.0.0" 421 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 422 | optional = true 423 | python-versions = ">=3.7" 424 | files = [ 425 | {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"}, 426 | {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"}, 427 | ] 428 | 429 | [package.extras] 430 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 431 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 432 | testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 433 | 434 | [[package]] 435 | name = "tomli" 436 | version = "2.0.1" 437 | description = "A lil' TOML parser" 438 | optional = false 439 | python-versions = ">=3.7" 440 | files = [ 441 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 442 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 443 | ] 444 | 445 | [[package]] 446 | name = "traitlets" 447 | version = "5.9.0" 448 | description = "Traitlets Python configuration system" 449 | optional = true 450 | python-versions = ">=3.7" 451 | files = [ 452 | {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, 453 | {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, 454 | ] 455 | 456 | [package.extras] 457 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 458 | test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] 459 | 460 | [[package]] 461 | name = "typing-extensions" 462 | version = "4.7.1" 463 | description = "Backported and Experimental Type Hints for Python 3.7+" 464 | optional = false 465 | python-versions = ">=3.7" 466 | files = [ 467 | {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, 468 | {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, 469 | ] 470 | 471 | [[package]] 472 | name = "virtualenv" 473 | version = "20.26.6" 474 | description = "Virtual Python Environment builder" 475 | optional = false 476 | python-versions = ">=3.7" 477 | files = [ 478 | {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, 479 | {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, 480 | ] 481 | 482 | [package.dependencies] 483 | distlib = ">=0.3.7,<1" 484 | filelock = ">=3.12.2,<4" 485 | importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""} 486 | platformdirs = ">=3.9.1,<5" 487 | 488 | [package.extras] 489 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 490 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 491 | 492 | [[package]] 493 | name = "wcwidth" 494 | version = "0.2.8" 495 | description = "Measures the displayed width of unicode strings in a terminal" 496 | optional = true 497 | python-versions = "*" 498 | files = [ 499 | {file = "wcwidth-0.2.8-py2.py3-none-any.whl", hash = "sha256:77f719e01648ed600dfa5402c347481c0992263b81a027344f3e1ba25493a704"}, 500 | {file = "wcwidth-0.2.8.tar.gz", hash = "sha256:8705c569999ffbb4f6a87c6d1b80f324bd6db952f5eb0b95bc07517f4c1813d4"}, 501 | ] 502 | 503 | [[package]] 504 | name = "zipp" 505 | version = "3.15.0" 506 | description = "Backport of pathlib-compatible object wrapper for zip files" 507 | optional = false 508 | python-versions = ">=3.7" 509 | files = [ 510 | {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, 511 | {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, 512 | ] 513 | 514 | [package.extras] 515 | docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 516 | testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] 517 | 518 | [extras] 519 | ipython = ["ipython"] 520 | 521 | [metadata] 522 | lock-version = "2.0" 523 | python-versions = "^3.7.9" 524 | content-hash = "aea003781ed7a7f294bc338647247121603041d48c775a6dfd91cefe3b706ae5" 525 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pdbr" 3 | version = "0.9.2" 4 | description = "Pdb with Rich library." 5 | authors = ["Can Sarigol "] 6 | packages = [ 7 | { include = "pdbr" } 8 | ] 9 | readme = "README.md" 10 | homepage = "https://github.com/cansarigol/pdbr" 11 | repository = "https://github.com/cansarigol/pdbr" 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "Operating System :: Microsoft :: Windows", 16 | "Operating System :: MacOS", 17 | "Operating System :: POSIX :: Linux", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | ] 23 | 24 | [tool.poetry.dependencies] 25 | python = "^3.7.9" 26 | rich = "*" 27 | ipython = {version = "*", optional = true} 28 | pyreadline3 = {version = "^3.4.1", markers = "sys_platform == 'win32'"} 29 | 30 | [tool.poetry.extras] 31 | ipython = ["ipython"] 32 | 33 | [tool.poetry.scripts] 34 | pdbr = 'pdbr.cli:shell' 35 | pdbr_telnet = 'pdbr.cli:telnet' 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | ruff = "^0.6.5" 39 | nox = "^2024.4.15" 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.2.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | 45 | [tool.vulture] 46 | make_whitelist = true 47 | min_confidence = 80 48 | paths = ["pdbr", "tests"] 49 | sort_by_size = true 50 | verbose = false 51 | 52 | [project] 53 | name = "pdbr" 54 | version = "0.9.2" 55 | 56 | [tool.setuptools] 57 | py-modules = [] 58 | 59 | [tool.ruff] 60 | line-length = 88 61 | 62 | [tool.ruff.lint] 63 | select = [ 64 | "E", # pycodestyle errors 65 | "W", # pycodestyle warnings 66 | "F", # pyflakes 67 | "I", # isort 68 | "B", # flake8-bugbear 69 | "C4", # flake8-comprehensions 70 | "PIE", # flake8-pie 71 | "ERA", # eradicate 72 | ] 73 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import django 5 | from django.conf import settings 6 | from django.test.utils import get_runner 7 | 8 | if __name__ == "__main__": 9 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.tests_django.test_settings" 10 | django.setup() 11 | TestRunner = get_runner(settings) 12 | test_runner = TestRunner() 13 | failures = test_runner.run_tests(["tests"]) 14 | sys.exit(bool(failures)) 15 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export SOURCE_FILES="pdbr tests noxfile.py" 4 | 5 | ruff check $SOURCE_FILES --fix 6 | black $SOURCE_FILES 7 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | pre-commit run --all-files 3 | 4 | poetry run nox --sessions test django_test 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = --capture=no --disable-warnings 3 | 4 | [pdbr] 5 | use_traceback= True 6 | style=dim 7 | store_history=.pdbr_history 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add '--skip-slow' cmdline option to skip tests that are marked with @pytest.mark.slow. 3 | """ 4 | 5 | import pytest 6 | 7 | 8 | def pytest_addoption(parser): 9 | parser.addoption( 10 | "--skip-slow", action="store_true", default=False, help="Skip slow tests" 11 | ) 12 | 13 | 14 | def pytest_collection_modifyitems(config, items): 15 | if not config.getoption("--skip-slow"): 16 | return 17 | skip_slow = pytest.mark.skip(reason="Specified --skip-slow") 18 | for item in items: 19 | if "slow" in item.keywords: 20 | item.add_marker(skip_slow) 21 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import pdbr 2 | 3 | 4 | def test_api_attr(): 5 | assert pdbr.__all__ == [ 6 | "set_trace", 7 | "run", 8 | "pm", 9 | "post_mortem", 10 | "celery_set_trace", 11 | "RichPdb", 12 | "pdbr_context", 13 | "apdbr_context", 14 | ] 15 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from tempfile import TemporaryDirectory 4 | 5 | import pytest 6 | 7 | from pdbr.utils import read_config 8 | 9 | root_dir = Path(__file__).parents[1] 10 | 11 | 12 | @pytest.fixture 13 | def dummy_global_config(): 14 | XDG_CONFIG_HOME = Path.home() / ".config" 15 | pdbr_dir = XDG_CONFIG_HOME / "pdbr" 16 | pdbr_dir.mkdir(exist_ok=True, parents=True) 17 | setup_file = pdbr_dir / "setup.cfg" 18 | backup_file = pdbr_dir / (setup_file.stem + ".cfg.bak") 19 | 20 | if setup_file.exists(): 21 | setup_file.rename(backup_file) 22 | 23 | with open(setup_file, "wt") as f: 24 | f.writelines(["[pdbr]\n", "theme = ansi_light"]) 25 | 26 | yield setup_file 27 | 28 | setup_file.unlink() 29 | 30 | if backup_file.exists(): 31 | backup_file.rename(setup_file) 32 | 33 | 34 | def test_global_config(dummy_global_config): 35 | assert dummy_global_config.exists() 36 | 37 | tmpdir = TemporaryDirectory() 38 | os.chdir(tmpdir.name) 39 | 40 | # Second element of tuple is theme 41 | assert read_config()[1] == "ansi_light" 42 | os.chdir(root_dir) 43 | 44 | 45 | def test_local_config(): 46 | tmpdir = TemporaryDirectory() 47 | os.chdir(tmpdir.name) 48 | setup_file = Path(tmpdir.name) / "setup.cfg" 49 | 50 | with open(setup_file, "wt") as f: 51 | f.writelines(["[pdbr]\n", "theme = ansi_dark"]) 52 | 53 | assert read_config()[1] == "ansi_dark" 54 | os.chdir(root_dir) 55 | 56 | 57 | def test_read_config(): 58 | pdbr_history = str(Path.home() / ".pdbr_history") 59 | pdbr_history_ipython = str(Path.home() / ".pdbr_history_ipython") 60 | 61 | assert read_config() == ("dim", None, pdbr_history, pdbr_history_ipython, None) 62 | -------------------------------------------------------------------------------- /tests/test_magic.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import re 3 | import sys 4 | from pathlib import Path 5 | 6 | import pytest 7 | from rich.console import Console 8 | from rich.theme import Theme 9 | 10 | from pdbr._pdbr import rich_pdb_klass 11 | 12 | NUMBER_RE = r"[\d.e+_,-]+" # Matches 1e+03, 1.0e-03, 1_000, 1,000 13 | 14 | TAG_RE = re.compile(r"\x1b[\[\]]+[\dDClhJt;?]+m?") 15 | 16 | 17 | def untag(s): 18 | """Not perfect, but does the job. 19 | >>> untag('\x1b[0mfoo\x1b[0m\x1b[0;34m(\x1b[0m\x1b[0marg\x1b[0m\x1b[0;34m)\x1b[0m' 20 | >>> '\x1b[0;34m\x1b[0m\x1b[0;34m\x1b[0m\x1b[0m') 21 | 'foo(arg)' 22 | """ 23 | s = s.replace("\x07", "") 24 | s = s.replace("\x1b[?2004l", "") 25 | return TAG_RE.sub("", s) 26 | 27 | 28 | def unquote(s): 29 | """ 30 | >>> unquote('"foo"') 31 | 'foo' 32 | >>> unquote('"foo"bar') 33 | '"foo"bar' 34 | """ 35 | for quote in ('"', "'"): 36 | if s.startswith(quote) and s.endswith(quote): 37 | return s[1:-1] 38 | return s 39 | 40 | 41 | TMP_FILE_CONTENT = '''def foo(arg): 42 | """Foo docstring""" 43 | pass 44 | ''' 45 | 46 | 47 | def import_tmp_file(rpdb, tmp_path: Path, file_content=TMP_FILE_CONTENT) -> Path: 48 | """Creates a temporary file, writes `file_content` to it and makes pdbr import it""" 49 | tmp_file = tmp_path / "foo.py" 50 | tmp_file.write_text(file_content) 51 | 52 | rpdb.onecmd(f'import sys; sys.path.append("{tmp_file.parent.absolute()}")') 53 | rpdb.onecmd(f"from {tmp_file.stem} import foo") 54 | return tmp_file 55 | 56 | 57 | @pytest.fixture 58 | def pdbr_child_process(tmp_path): 59 | """ 60 | Spawn a pdbr prompt in a child process. 61 | """ 62 | from pexpect import spawn 63 | 64 | file = tmp_path / "foo.py" 65 | file.write_text("import pdbr;breakpoint()") 66 | 67 | child = spawn( 68 | str(Path(sys.executable)), 69 | [str(file)], 70 | encoding="utf-8", 71 | ) 72 | child.expect("breakpoint") 73 | child.timeout = 10 74 | return child 75 | 76 | 77 | @pytest.fixture 78 | def RichIPdb(): 79 | """ 80 | In contrast to the normal RichPdb in test_pdbr.py which inherits from 81 | built-in pdb.Pdb, this one inherits from IPython's TerminalPdb, which holds 82 | a 'shell' attribute that is a IPython TerminalInteractiveShell. 83 | This is required for the magic commands to work (and happens automatically 84 | when the user runs pdbr when IPython is importable). 85 | """ 86 | from IPython.terminal.debugger import TerminalPdb 87 | 88 | currentframe = inspect.currentframe() 89 | 90 | def rich_ipdb_klass(*args, **kwargs): 91 | ripdb = rich_pdb_klass(TerminalPdb, show_layouts=False)(*args, **kwargs) 92 | # Set frame and stack related self-attributes 93 | ripdb.botframe = currentframe.f_back 94 | ripdb.setup(currentframe.f_back, None) 95 | # Set the console's file to stdout so that we can capture the output 96 | _console = Console( 97 | file=kwargs.get("stdout", sys.stdout), 98 | theme=Theme( 99 | {"info": "dim cyan", "warning": "magenta", "danger": "bold red"} 100 | ), 101 | ) 102 | ripdb._console = _console 103 | return ripdb 104 | 105 | return rich_ipdb_klass 106 | 107 | 108 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="pexpect") 109 | @pytest.mark.slow 110 | class TestPdbrChildProcess: 111 | def test_time(self, pdbr_child_process): 112 | pdbr_child_process.sendline("from time import sleep") 113 | pdbr_child_process.sendline("%time sleep(0.1)") 114 | pdbr_child_process.expect(re.compile("CPU times: .+")) 115 | pdbr_child_process.expect("Wall time: .+") 116 | 117 | def test_timeit(self, pdbr_child_process): 118 | pdbr_child_process.sendline("%timeit -n 1 -r 1 pass") 119 | pdbr_child_process.expect_exact("std. dev. of 1 run, 1 loop each)") 120 | 121 | 122 | @pytest.mark.skipif(sys.platform.startswith("win"), reason="pexpect") 123 | class TestPdbrMagic: 124 | def test_onecmd_time_line_magic(self, capsys, RichIPdb): 125 | RichIPdb().onecmd("%time pass") 126 | captured = capsys.readouterr() 127 | output = captured.out 128 | assert re.search( 129 | f"CPU times: user {NUMBER_RE} [mµn]s, " 130 | f"sys: {NUMBER_RE} [mµn]s, " 131 | f"total: {NUMBER_RE} [mµn]s\n" 132 | f"Wall time: {NUMBER_RE} [mµn]s", 133 | output, 134 | ) 135 | 136 | def test_onecmd_unsupported_cell_magic(self, capsys, RichIPdb): 137 | RichIPdb().onecmd("%%time pass") 138 | captured = capsys.readouterr() 139 | output = captured.out 140 | error = ( 141 | "Cell magics (multiline) are not yet supported. Use a single '%' instead." 142 | ) 143 | assert output == "*** " + error + "\n" 144 | cmd = "%%time" 145 | stop = RichIPdb().onecmd(cmd) 146 | captured_output = capsys.readouterr().out 147 | assert not stop 148 | RichIPdb().error(error) 149 | cell_magics_error = capsys.readouterr().out 150 | assert cell_magics_error == captured_output 151 | 152 | def test_onecmd_lsmagic_line_magic(self, capsys, RichIPdb): 153 | RichIPdb().onecmd("%lsmagic") 154 | captured = capsys.readouterr() 155 | output = captured.out 156 | 157 | assert re.search( 158 | "Available line magics:\n%alias +%alias_magic +%autoawait.*%%writefile", 159 | output, 160 | re.DOTALL, 161 | ) 162 | 163 | def test_no_zombie_lastcmd(self, capsys, RichIPdb): 164 | rpdb = RichIPdb(stdout=sys.stdout) 165 | rpdb.onecmd("print('SHOULD_NOT_BE_IN_%pwd_OUTPUT')") 166 | captured = capsys.readouterr() 167 | assert captured.out.endswith( 168 | "SHOULD_NOT_BE_IN_%pwd_OUTPUT\n" 169 | ) # Starts with colors and prompt 170 | rpdb.onecmd("%pwd") 171 | captured = capsys.readouterr() 172 | assert captured.out.endswith(Path.cwd().absolute().as_posix() + "\n") 173 | assert "SHOULD_NOT_BE_IN_%pwd_OUTPUT" not in captured.out 174 | 175 | def test_IPython_Pdb_magics_implementation(self, tmp_path, capsys, RichIPdb): 176 | """ 177 | We test do_{magic} methods that are concretely implemented by 178 | IPython.core.debugger.Pdb, and don't default to IPython's 179 | 'InteractiveShell.run_line_magic()' like the other magics. 180 | """ 181 | from IPython.utils.text import dedent 182 | 183 | rpdb = RichIPdb(stdout=sys.stdout) 184 | tmp_file = import_tmp_file(rpdb, tmp_path) 185 | 186 | # pdef 187 | rpdb.do_pdef("foo") 188 | do_pdef_foo_output = capsys.readouterr().out 189 | untagged = untag(do_pdef_foo_output).strip() 190 | assert untagged.endswith("foo(arg)"), untagged 191 | rpdb.onecmd("%pdef foo") 192 | magic_pdef_foo_output = capsys.readouterr().out 193 | untagged = untag(magic_pdef_foo_output).strip() 194 | assert untagged.endswith("foo(arg)"), untagged 195 | 196 | # pdoc 197 | rpdb.onecmd("%pdoc foo") 198 | magic_pdef_foo_output = capsys.readouterr().out 199 | untagged = untag(magic_pdef_foo_output).strip() 200 | expected_docstring = dedent( 201 | """Class docstring: 202 | Foo docstring 203 | Call docstring: 204 | Call self as a function.""" 205 | ) 206 | assert untagged == expected_docstring, untagged 207 | 208 | # pfile 209 | rpdb.onecmd("%pfile foo") 210 | magic_pfile_foo_output = capsys.readouterr().out 211 | untagged = untag(magic_pfile_foo_output).strip() 212 | tmp_file_content = Path(tmp_file).read_text().strip() 213 | assert untagged == tmp_file_content 214 | 215 | # pinfo 216 | rpdb.onecmd("%pinfo foo") 217 | magic_pinfo_foo_output = capsys.readouterr().out 218 | untagged = untag(magic_pinfo_foo_output).strip() 219 | expected_pinfo = dedent( 220 | f"""Signature: foo(arg) 221 | Docstring: Foo docstring 222 | File: {tmp_file.absolute()} 223 | Type: function""" 224 | ) 225 | assert untagged == expected_pinfo, untagged 226 | 227 | # pinfo2 228 | rpdb.onecmd("%pinfo2 foo") 229 | magic_pinfo2_foo_output = capsys.readouterr().out 230 | untagged = untag(magic_pinfo2_foo_output).strip() 231 | expected_pinfo2 = re.compile( 232 | dedent( 233 | rf"""Signature: foo\(arg\) 234 | Source:\s* 235 | %s 236 | File: {tmp_file.absolute()} 237 | Type: function""" 238 | ) 239 | % re.escape(tmp_file_content) 240 | ) 241 | assert expected_pinfo2.fullmatch(untagged), untagged 242 | 243 | # psource 244 | rpdb.onecmd("%psource foo") 245 | magic_psource_foo_output = capsys.readouterr().out 246 | untagged = untag(magic_psource_foo_output).strip() 247 | expected_psource = 'def foo(arg):\n """Foo docstring"""\n pass' 248 | assert untagged == expected_psource, untagged 249 | 250 | def test_expr_questionmark_pinfo(self, tmp_path, capsys, RichIPdb): 251 | from IPython.utils.text import dedent 252 | 253 | rpdb = RichIPdb(stdout=sys.stdout) 254 | tmp_file = import_tmp_file(rpdb, tmp_path) 255 | # pinfo 256 | rpdb.onecmd(rpdb.precmd("foo?")) 257 | magic_foo_qmark_output = capsys.readouterr().out 258 | untagged = untag(magic_foo_qmark_output).strip() 259 | 260 | expected_pinfo_path = ( 261 | f"/private/var/folders/.*/{tmp_file.name}" 262 | if sys.platform == "darwin" 263 | else f"/tmp/.*/{tmp_file.name}" 264 | ) 265 | expected_pinfo = re.compile( 266 | dedent( 267 | rf""".*Signature: foo\(arg\) 268 | Docstring: Foo docstring 269 | File: {expected_pinfo_path} 270 | Type: function""" 271 | ) 272 | ) 273 | assert expected_pinfo.fullmatch(untagged), f"untagged = {untagged!r}" 274 | 275 | # pinfo2 276 | rpdb.onecmd(rpdb.precmd("foo??")) 277 | magic_foo_qmark2_output = capsys.readouterr().out 278 | rpdb.onecmd(rpdb.precmd("%pinfo2 foo")) 279 | magic_pinfo2_foo_output = capsys.readouterr().out 280 | assert magic_pinfo2_foo_output == magic_foo_qmark2_output 281 | 282 | def test_filesystem_magics(self, capsys, RichIPdb): 283 | cwd = Path.cwd().absolute().as_posix() 284 | rpdb = RichIPdb(stdout=sys.stdout) 285 | rpdb.onecmd("%pwd") 286 | pwd_output = capsys.readouterr().out.strip() 287 | assert pwd_output == cwd 288 | rpdb.onecmd("import os; os.getcwd()") 289 | pwd_output = unquote(capsys.readouterr().out.strip()) 290 | assert pwd_output == cwd 291 | 292 | new_dir = str(Path.cwd().absolute().parent) 293 | rpdb.onecmd(f"%cd {new_dir}") 294 | cd_output = untag(capsys.readouterr().out.strip()) 295 | assert cd_output.endswith(new_dir) 296 | rpdb.onecmd("%pwd") 297 | pwd_output = capsys.readouterr().out.strip() 298 | assert pwd_output == new_dir 299 | rpdb.onecmd("import os; os.getcwd()") 300 | pwd_output = unquote(capsys.readouterr().out.strip()) 301 | assert pwd_output == new_dir 302 | -------------------------------------------------------------------------------- /tests/test_pdbr.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import pdb 3 | 4 | import pytest 5 | 6 | from pdbr._pdbr import rich_pdb_klass 7 | 8 | 9 | @pytest.fixture 10 | def RichPdb(*args, **kwargs): 11 | currentframe = inspect.currentframe() 12 | 13 | def wrapper(): 14 | rpdb = rich_pdb_klass(pdb.Pdb, show_layouts=False)(*args, **kwargs) 15 | # Set frame and stack related self-attributes 16 | rpdb.botframe = currentframe.f_back 17 | rpdb.setup(currentframe.f_back, None) 18 | return rpdb 19 | 20 | return wrapper 21 | 22 | 23 | def test_prompt(RichPdb): 24 | assert RichPdb().prompt == "(Pdbr) " 25 | 26 | 27 | def test_print(capsys, RichPdb): 28 | RichPdb()._print("msg") 29 | captured = capsys.readouterr() 30 | assert captured.out == "msg\n" 31 | 32 | 33 | def test_print_error(capsys, RichPdb): 34 | RichPdb().error("error") 35 | captured = capsys.readouterr() 36 | assert captured.out == "\x1b[1;31m*** error\x1b[0m\n" 37 | 38 | 39 | def test_print_with_style(capsys, RichPdb): 40 | RichPdb()._print("msg", style="yellow") 41 | captured = capsys.readouterr() 42 | assert captured.out == "\x1b[33mmsg\x1b[0m\n" 43 | 44 | 45 | def test_print_without_escape_tag(capsys, RichPdb): 46 | RichPdb()._print("[blue]msg[/]") 47 | captured = capsys.readouterr() 48 | assert captured.out == "\x1b[34mmsg\x1b[0m\n" 49 | 50 | 51 | def test_print_array(capsys, RichPdb): 52 | RichPdb()._print("[[8]]") 53 | captured = capsys.readouterr() 54 | assert ( 55 | captured.out == "\x1b[1m[\x1b[0m\x1b[1m[\x1b[0m\x1b[1;36m8" 56 | "\x1b[0m\x1b[1m]\x1b[0m\x1b[1m]\x1b[0m\n" 57 | ) 58 | 59 | 60 | def test_onecmd(capsys, RichPdb): 61 | rpdb = RichPdb() 62 | cmd = 'print("msg")' 63 | stop = rpdb.onecmd(cmd) 64 | captured = capsys.readouterr() 65 | assert not stop 66 | assert captured.out == "msg\n" 67 | -------------------------------------------------------------------------------- /tests/tests_django/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cansarigol/pdbr/2200ff06c3548c92260a9cc984d4001543c36809/tests/tests_django/__init__.py -------------------------------------------------------------------------------- /tests/tests_django/test_settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | BASE_DIR = Path(__file__).absolute().parents[1] 4 | 5 | SECRET_KEY = "fake-key" 6 | DATABASES = { 7 | "default": { 8 | "ENGINE": "django.db.backends.sqlite3", 9 | "NAME": str(BASE_DIR / "db.sqlite3"), 10 | } 11 | } 12 | 13 | INSTALLED_APPS = ("tests.tests_django",) 14 | 15 | TEST_RUNNER = "pdbr.runner.PdbrDiscoverRunner" 16 | 17 | ROOT_URLCONF = "tests.tests_django.urls" 18 | 19 | MIDDLEWARE = ["pdbr.middlewares.django.PdbrMiddleware"] 20 | -------------------------------------------------------------------------------- /tests/tests_django/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class DjangoTest(TestCase): 5 | def test_runner(self): 6 | self.assertEqual("foo", "foo") 7 | 8 | def test_middleware(self): 9 | response = self.client.get("") 10 | self.assertEqual(response.status_code, 200) 11 | -------------------------------------------------------------------------------- /tests/tests_django/urls.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import path 3 | 4 | urlpatterns = [ 5 | path("", lambda request: HttpResponse()), 6 | ] 7 | --------------------------------------------------------------------------------