Repository: cansarigol/pdbr Branch: master Commit: a85df2d320fd Files: 37 Total size: 59.0 KB Directory structure: gitextract_rmp4r7u5/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode/ │ └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── 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 ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - '*' workflow_dispatch: jobs: build-n-publish: name: Build and publish to PyPI runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python 3.10.16 uses: actions/setup-python@v4 with: python-version: '3.10.16' - name: Install poetry run: python -m pip install poetry --user - name: Build run: poetry build - name: Publish to PyPI env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_PASSWORD }} run: poetry publish ================================================ FILE: .github/workflows/tests.yml ================================================ name: Test on: push: branches: ["master"] pull_request: branches: ["master"] jobs: check: name: "Check" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install pre-commit run: | pip install --upgrade pre-commit - name: Run check run: | pre-commit run --all-files test: name: "Tests" runs-on: ${{ matrix.platform }} needs: check strategy: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install nox run: | pip install --upgrade nox - name: Run tests run: | nox --sessions test django_test ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # lint .ruff_cache ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: python-use-type-annotations - repo: https://github.com/jendrikseipp/vulture rev: v2.14 hooks: - id: vulture - repo: https://github.com/astral-sh/ruff-pre-commit rev: 'v0.9.3' hooks: - id: ruff - repo: https://github.com/psf/black rev: 24.10.0 hooks: - id: black language_version: python3 ================================================ FILE: .vscode/settings.json ================================================ { "makefile.extensionOutputFolder": "./.vscode" } ================================================ FILE: Dockerfile ================================================ FROM python:3.7.9 ENV PYTHONUNBUFFERED=0 RUN pip install pip \ && pip install nox \ && pip install pre-commit WORKDIR /pdbr COPY . . RUN pre-commit run --all-files RUN nox --sessions test django_test ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Can Sarıgöl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ lint: sh scripts/lint test: sh scripts/test celery: celery -A tasks worker --loglevel=info build: docker build -t pdbr . act: act -r -j test --container-architecture linux/amd64 ================================================ FILE: README.md ================================================ # pdbr [![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) `pdbr` is intended to make the PDB results more colorful. it uses [Rich](https://github.com/willmcgugan/rich) library to carry out that. ## Installing Install with `pip` or your favorite PyPi package manager. ``` pip install pdbr ``` ## Breakpoint In order to use ```breakpoint()```, set **PYTHONBREAKPOINT** with "pdbr.set_trace" ```python import os os.environ["PYTHONBREAKPOINT"] = "pdbr.set_trace" ``` or just import pdbr ```python import pdbr ``` ## New commands ### (i)nspect / inspectall | ia [rich.inspect](https://rich.readthedocs.io/en/latest/introduction.html?s=03#rich-inspector) ### search | src Search a phrase in the current frame. In order to repeat the last one, type **/** character as arg. ### sql Display value in sql format. Don't forget to install [sqlparse](https://github.com/andialbrecht/sqlparse) package. ![](/images/image13.png) It can be used for Django model queries as follows. ``` >>> sql str(Users.objects.all().query) ``` ![](/images/image14.png) ### (syn)tax [ val,lexer ] Display [lexer](https://pygments.org/docs/lexers/). ### (v)ars Get the local variables list as table. ### varstree | vt Get the local variables list as tree. ![](/images/image5.png) ## Config 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`. ### Style In order to use Rich's traceback, style, and theme: ``` [pdbr] style = yellow use_traceback = True theme = friendly ``` Also custom `Console` object can be assigned to the `set_trace`. ```python import pdbr from rich.console import Console from rich.style import Style from rich.theme import Theme custom_theme = Theme({ "info": "dim cyan", "warning": "magenta", "danger": "bold red", }) custom_style = Style( color="magenta", bgcolor="yellow", italic=True, ) console = Console(theme=custom_theme, style=custom_style) pdbr.set_trace(console=console) ``` ### History **store_history** setting is used to keep and reload history, even the prompt is closed and opened again: ``` [pdbr] ... store_history=.pdbr_history ``` By default, history is stored globally in `~/.pdbr_history`. ### Context The **context** setting is used to specify the number of lines of source code context to show when displaying stacktrace information. ``` [pdbr] ... context=10 ``` This setting is only available when using `pdbr` with `IPython`. ## Celery 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). ```python from celery import Celery app = Celery('tasks', broker='pyamqp://guest@localhost//') @app.task def add(x, y): import pdbr; pdbr.celery_set_trace() return x + y ``` #### Telnet Instead of using `telnet` or `nc`, in terms of using pdbr style, `pdbr_telnet` command can be used. ![](/images/image6.png) Also in order to activate history and be able to use arrow keys, install and use [rlwrap](https://github.com/hanslub42/rlwrap) package. ``` rlwrap -H '~/.pdbr_history' pdbr_telnet localhost 6899 ``` ## IPython `pdbr` integrates with [IPython](https://ipython.readthedocs.io/). This makes [`%magics`](https://ipython.readthedocs.io/en/stable/interactive/magics.html) available, for example: ```python (Pdbr) %timeit range(100) 104 ns ± 2.05 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) ``` To enable `IPython` features, install it separately, or like below: ``` pip install pdbr[ipython] ``` ## pytest In order to use `pdbr` with pytest `--pdb` flag, add `addopts` setting in your pytest.ini. ``` [pytest] addopts: --pdbcls=pdbr:RichPdb ``` ## sys.excepthook 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. 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: ```python import sys import pdbr def custom_excepthook(exc_type, exc_value, exc_traceback): pdbr.post_mortem(exc_traceback, exc_value) # [Optional] call the original excepthook as well sys.__excepthook__(exc_type, exc_value, exc_traceback) sys.excepthook = custom_excepthook ``` Now, whenever an unhandled exception occurs, `pdbr` will be triggered, allowing you to debug the issue interactively. ## Context Decorator `pdbr_context` and `apdbr_context` (`asyncio` corresponding) can be used as **with statement** or **decorator**. It calls `post_mortem` if `traceback` is not none. ```python from pdbr import apdbr_context, pdbr_context @pdbr_context() def foo(): ... def bar(): with pdbr_context(): ... @apdbr_context() async def foo(): ... async def bar(): async with apdbr_context(): ... ``` ![](/images/image12.png) ## Django DiscoverRunner 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. ``` TEST_RUNNER = "pdbr.runner.PdbrDiscoverRunner" ``` ![](/images/image10.png) ## Middlewares ### Starlette ```python from fastapi import FastAPI from pdbr.middlewares.starlette import PdbrMiddleware app = FastAPI() app.add_middleware(PdbrMiddleware, debug=True) @app.get("/") async def main(): 1 / 0 return {"message": "Hello World"} ``` ### Django In order to catch the problematic codes with post mortem, place the middleware class. ``` MIDDLEWARE = ( ... "pdbr.middlewares.django.PdbrMiddleware", ) ``` ![](/images/image11.png) ## Shell 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`. #### %debug magic sample ![](/images/image9.png) ### As a Script 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. ```python # equivalent code: `python -m pdbr -c 'b 5' my_test.py` pdbr -c 'b 5' my_test.py >>> Breakpoint 1 at /my_test.py:5 > /my_test.py(3)() 1 2 ----> 3 def test(): 4 foo = "foo" 1 5 bar = "bar" (Pdbr) ``` ### Terminal #### Django shell sample ![](/images/image7.png) ## Vscode user snippet To create or edit your own snippets, select **User Snippets** under **File > Preferences** (**Code > Preferences** on macOS), and then select **python.json**. Place the below snippet in json file for **pdbr**. ``` { ... "pdbr": { "prefix": "pdbr", "body": "import pdbr; pdbr.set_trace()", "description": "Code snippet for pdbr debug" }, } ``` For **Celery** debug. ``` { ... "rdbr": { "prefix": "rdbr", "body": "import pdbr; pdbr.celery_set_trace()", "description": "Code snippet for Celery pdbr debug" }, } ``` ## Samples ![](/images/image1.png) ![](/images/image3.png) ![](/images/image4.png) ### Traceback ![](/images/image2.png) ================================================ FILE: noxfile.py ================================================ import nox nox.options.stop_on_first_error = True @nox.session def test(session, reuse_venv=True): session.install( ".", "pytest", "pytest-cov", "rich", "prompt_toolkit", "IPython", ) session.run( "pytest", "--cov-report", "term-missing", "--cov=pdbr", "--capture=no", "--disable-warnings", "tests", ) @nox.session @nox.parametrize("django", ["3.2", "4.2"]) def django_test(session, django, reuse_venv=True): session.install(f"django=={django}", "rich", "pytest") session.run("python", "runtests.py") ================================================ FILE: pdbr/__init__.py ================================================ from pdbr.__main__ import RichPdb, celery_set_trace, pm, post_mortem, run, set_trace from pdbr._cm import apdbr_context, pdbr_context __all__ = [ "set_trace", "run", "pm", "post_mortem", "celery_set_trace", "RichPdb", "pdbr_context", "apdbr_context", ] ================================================ FILE: pdbr/__main__.py ================================================ import os import pdb import sys from .utils import _pdbr_cls, _rdbr_cls os.environ["PYTHONBREAKPOINT"] = "pdbr.set_trace" RichPdb = _pdbr_cls(return_instance=False, show_layouts=False) def set_trace(*, console=None, header=None, context=None, show_layouts=False): pdb_cls = _pdbr_cls(console=console, context=context, show_layouts=show_layouts) if header is not None: pdb_cls.message(header) pdb_cls.set_trace(sys._getframe().f_back) def run(statement, globals=None, locals=None): RichPdb().run(statement, globals, locals) def post_mortem(traceback=None, value=None): _, sys_value, sys_traceback = sys.exc_info() value = value or sys_value traceback = traceback or sys_traceback if traceback is None: raise ValueError( "A valid traceback must be passed if no exception is being handled" ) p = RichPdb() p.reset() if value: p.error(value) p.interaction(None, traceback) def pm(): post_mortem(sys.last_traceback) def celery_set_trace(frame=None): pdb_cls = _rdbr_cls() if frame is None: frame = sys._getframe().f_back return pdb_cls.set_trace(frame) def main(): pdb.Pdb = RichPdb pdb.main() if __name__ == "__main__": main() ================================================ FILE: pdbr/_cm.py ================================================ from contextlib import ContextDecorator from functools import wraps from pdbr.__main__ import post_mortem class pdbr_context(ContextDecorator): def __init__(self, suppress_exc=True, debug=True): self.suppress_exc = suppress_exc self.debug = debug def __enter__(self): return self def __exit__(self, _, exc_value, exc_traceback): if exc_traceback and self.debug: post_mortem(exc_traceback, exc_value) return self.suppress_exc return False class AsyncContextDecorator(ContextDecorator): def __call__(self, func): @wraps(func) async def inner(*args, **kwds): async with self._recreate_cm(): return await func(*args, **kwds) return inner class apdbr_context(AsyncContextDecorator): def __init__(self, suppress_exc=True, debug=True): self.suppress_exc = suppress_exc self.debug = debug async def __aenter__(self): return self async def __aexit__(self, _, exc_value, exc_traceback): if exc_traceback and self.debug: post_mortem(exc_traceback, exc_value) return self.suppress_exc return False ================================================ FILE: pdbr/_console_layout.py ================================================ from rich.containers import Lines from rich.errors import NotRenderableError from rich.layout import Layout from rich.panel import Panel class ConsoleLayoutMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class ConsoleLayout(metaclass=ConsoleLayoutMeta): def __init__(self, console): self.console = console self.layout = self._prep_layout() def _prep_layout(self): layout = Layout() right_body = Layout(name="right_body", ratio=1) layout.split( Layout(name="left_body", ratio=2), right_body, splitter="row", ) right_body.split( Layout(name="up_footer", ratio=2), Layout(name="bottom_footer", ratio=1) ) return layout def print(self, message, code, stack_trace, vars, **kwargs): try: self.layout["left_body"].update(code) self.layout["up_footer"].update(Panel(vars, title="Locals")) self.layout["bottom_footer"].update( Panel(Lines(stack_trace), title="Stack", style="white on blue") ) self.console.print(self.layout, **kwargs) self.console.print(message, **kwargs) except NotRenderableError: self.console.print(message, **kwargs) ================================================ FILE: pdbr/_pdbr.py ================================================ import inspect import io import re from pathlib import Path from pdb import Pdb from rich import box, markup from rich._inspect import Inspect from rich.console import Console from rich.panel import Panel from rich.pretty import pprint from rich.syntax import DEFAULT_THEME, Syntax from rich.table import Table from rich.text import Text from rich.theme import Theme from rich.tree import Tree from pdbr._console_layout import ConsoleLayout try: from IPython.terminal.interactiveshell import TerminalInteractiveShell TerminalInteractiveShell.simple_prompt = False except ImportError: pass WITHOUT_LAYOUT_COMMANDS = ( "where", "w", ) ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") class AsciiStdout(io.TextIOWrapper): pass def rich_pdb_klass( base, is_celery=False, console=None, context=None, show_layouts=True ): class RichPdb(base): _style = None _theme = None _history_file = None _ipython_history_file = None _latest_search_arg = "" def __init__( self, completekey="tab", stdin=None, stdout=None, skip=None, nosigint=False, readrc=True, ): init_kwargs = ( {"out": stdout} if is_celery else { "completekey": completekey, "stdin": stdin, "stdout": stdout, "skip": skip, "nosigint": nosigint, "readrc": readrc, } ) if console is not None: self._console = console if context is not None: if base == Pdb: raise ValueError("Context can only be used with IPython") init_kwargs["context"] = context super().__init__(**init_kwargs) self.prompt = "(Pdbr) " def pt_init(self, pt_session_options=None): from prompt_toolkit.history import FileHistory if self._ipython_history_file: history_file = FileHistory(self._ipython_history_file) self.shell.debugger_history = history_file # In order to fix the error for ipython 8.x self.debugger_history = history_file func = super().pt_init func_args = inspect.getfullargspec(super().pt_init).args if "pt_session_options" in func_args: func(pt_session_options) else: func() @property def console(self): if not hasattr(self, "_console"): self._console = Console( file=( AsciiStdout(buffer=self.stdout.buffer, encoding="ascii") if is_celery else self.stdout ), theme=Theme( {"info": "dim cyan", "warning": "magenta", "danger": "bold red"} ), style=self._style, force_terminal=True, force_interactive=True, ) return self._console def do_help(self, arg): super().do_help(arg) if not arg: self._print( Panel( "Visit " "[bold][link=https://github.com/cansarigol/pdbr]" "https://github.com/cansarigol/pdbr[/link][/]" " for more!" ), style="warning", print_layout=False, ) do_help.__doc__ = base.do_help.__doc__ do_h = do_help def _get_syntax_for_list(self, line_range=None): if not line_range: first = max(1, self.curframe.f_lineno - 5) line_range = first, first + 10 filename = self.curframe.f_code.co_filename highlight_lines = {self.curframe.f_lineno} return Syntax.from_path( filename, line_numbers=True, theme=self._theme or DEFAULT_THEME, line_range=line_range, highlight_lines=highlight_lines, ) def _get_variables(self): try: return [ (k, str(v), str(type(v))) for k, v in self.curframe.f_locals.items() if not k.startswith("__") and k != "pdbr" ] except AttributeError: return [] def do_l(self, arg): """l List 11 lines source code for the current file. """ try: self._print(self._get_syntax_for_list(), print_layout=False) except BaseException: self.error("could not get source code") def do_longlist(self, arg): """longlist | ll List the whole source code for the current function or frame. """ try: lines, lineno = self._getsourcelines(self.curframe) last = lineno + len(lines) self._print( self._get_syntax_for_list((lineno, last)), print_layout=False ) except BaseException: self.error("could not get source code") do_ll = do_longlist def do_source(self, arg): """source expression Try to get source code for the given object and display it. """ try: obj = self._getval(arg) lines, lineno = self._getsourcelines(obj) last = lineno + len(lines) self._print( self._get_syntax_for_list((lineno, last)), print_layout=False ) except BaseException as err: self.error(err) def do_search(self, arg): """search | src Search a phrase in the current frame. In order to repeat the last one, type `/` character as arg. """ if not arg or (arg == "/" and not self._latest_search_arg): self.error("Search failed: arg is missing") return if arg == "/": arg = self._latest_search_arg else: self._latest_search_arg = arg lines, lineno = self._getsourcelines(self.curframe) indexes = [index for index, line in enumerate(lines, lineno) if arg in line] if len(indexes) > 0: bigger_indexes = [ index for index in indexes if index > self.curframe.f_lineno ] next_line = bigger_indexes[0] if bigger_indexes else indexes[0] return super().do_jump(next_line) else: self.error(f"Search failed: '{arg}' not found") do_src = do_search def _getsourcelines(self, obj): lines, lineno = inspect.getsourcelines(obj) lineno = max(1, lineno) return lines, lineno def get_varstable(self): variables = self._get_variables() if not variables: return table = Table(title="List of local variables", box=box.MINIMAL) table.add_column("Variable", style="cyan") table.add_column("Value", style="magenta") table.add_column("Type", style="green") [ table.add_row(variable, value, _type) for variable, value, _type in variables ] return table def do_v(self, arg): """v(ars) List of local variables """ self._print(self.get_varstable(), print_layout=False) def get_varstree(self): variables = self._get_variables() if not variables: return tree_key = "" type_tree = None tree = Tree("Variables") for variable, value, _type in sorted( variables, key=lambda item: (item[2], item[0]) ): if tree_key != _type: if tree_key != "": tree.add(type_tree, style="bold green") type_tree = Tree(_type) tree_key = _type type_tree.add(f"{variable}: {value}", style="magenta") if type_tree: tree.add(type_tree, style="bold green") return tree def do_varstree(self, arg): """varstree | vt List of local variables in Rich.Tree """ self._print(self.get_varstree(), print_layout=False) do_vt = do_varstree def do_inspect(self, arg, all=False): """(i)nspect Display the data / methods / docs for any Python object. """ try: self._print( Inspect(self._getval(arg), methods=True, all=all), print_layout=False, ) except BaseException: pass def do_inspectall(self, arg): """inspectall | ia Inspect with all to see all attributes. """ self.do_inspect(arg, all=True) do_i = do_inspect do_ia = do_inspectall def do_pp(self, arg): """pp expression Rich pretty print. """ try: pprint(self._getval(arg), console=self.console) except BaseException: pass def do_syntax(self, arg): """syn(tax)[ val,lexer ] Display lexer. https://pygments.org/docs/lexers/ """ try: val, lexer = arg.split(",") val = val.strip() lexer = lexer.strip() val = Syntax( self._getval(val), self._getval(lexer), theme=self._theme or DEFAULT_THEME, ) self._print(val) except BaseException: pass do_syn = do_syntax def do_sql(self, arg): """sql Display value in sql format. """ try: import sqlparse val = sqlparse.format( self._getval(arg), reindent=True, keyword_case="upper" ) self._print(val) except ModuleNotFoundError as error: raise type(error)("Install sqlparse to see sql format.") from error def displayhook(self, obj): if obj is not None: self._print(obj if isinstance(obj, (dict, list)) else repr(obj)) def error(self, msg): self._print(msg, prefix="***", style="danger", print_layout=False) def _format_stack_entry(self, frame_lineno): stack_entry = Pdb.format_stack_entry(self, frame_lineno, "\n") return stack_entry.replace(str(Path.cwd().absolute()), "") def stack_trace(self): stacks = [] try: for frame_lineno in self.stack: frame, _ = frame_lineno if frame is self.curframe: prefix = "-> " else: prefix = " " stack_entry = self._format_stack_entry(frame_lineno) first_line, _ = stack_entry.splitlines() text_body = Text(stack_entry) text_prefix = Text(prefix) text_body.stylize("bold", len(first_line), len(stack_entry)) text_prefix.stylize("bold") stacks.append(Text.assemble(text_prefix, text_body)) except KeyboardInterrupt: pass return reversed(stacks) def message(self, msg): "this is used by the upstream PDB class" self._print(msg) def precmd(self, line): # Python 3.13+: Ctrl-D comes as literal "EOF" if line is None or line == "EOF": return "continue" if line.endswith("??"): line = "pinfo2 " + line[:-2] elif line.endswith("?"): line = "pinfo " + line[:-1] elif line.startswith("%"): if line.startswith("%%"): self.error( "Cell magics (multiline) are not yet supported. " "Use a single '%' instead." ) return self.run_magic(line[1:]) return super().precmd(line) def _print(self, val, prefix=None, style=None, print_layout=True): if val == "--Return--": return if isinstance(val, str) and ("[0m" in val or "[/" in val): val = markup.render(val) kwargs = {"style": str(style)} if style else {} args = (prefix, val) if prefix else (val,) if ( show_layouts and print_layout and self.lastcmd not in WITHOUT_LAYOUT_COMMANDS ): self._print_layout(*args, **kwargs) else: self.console.print(*args, **kwargs) def _print_layout(self, val, **kwargs): ConsoleLayout(self.console).print( val, code=self._get_syntax_for_list(), stack_trace=self.stack_trace(**kwargs), vars=self.get_varstree(), **kwargs, ) def print_stack_entry(self, frame_lineno, prompt_prefix="\n-> "): def print_syntax(*args): # Remove color format. self._print( Syntax( ANSI_ESCAPE.sub("", self.format_stack_entry(*args)), "python", theme=self._theme or DEFAULT_THEME, ), print_layout=False, ) if is_celery: Pdb.print_stack_entry(self, frame_lineno, prompt_prefix) elif base == Pdb: print_syntax(frame_lineno, prompt_prefix) else: print_syntax(frame_lineno, "") # vds: >> frame, lineno = frame_lineno filename = frame.f_code.co_filename self.shell.hooks.synchronize_with_editor(filename, lineno, 0) # vds: << def print_stack_trace(self, count): """ Use pdb stack trace due to hide hidden frames IPython is not using traceback count (for only python3.14), """ Pdb.print_stack_trace(self, count=count) def run_magic(self, line) -> str: """ Parses the line and runs the appropriate magic function. Assumes that the line is without a leading '%'. """ magic_name, arg, line = self.parseline(line) if hasattr(self, f"do_{magic_name}"): # We want to use do_{magic_name} methods if defined. # This is indeed the case with do_pdef, do_pdoc etc, # which are defined by our base class (IPython.core.debugger.Pdb). result = getattr(self, f"do_{magic_name}")(arg) else: magic_fn = self.shell.find_line_magic(magic_name) if not magic_fn: self.error(f"Line Magic %{magic_name} not found") return "" if magic_name in ("time", "timeit"): result = magic_fn( arg, local_ns={**self.curframe_locals, **self.curframe.f_globals}, ) else: result = magic_fn(arg) if result: result = str(result) self._print(result) return "" return RichPdb ================================================ FILE: pdbr/cli.py ================================================ import sys from telnetlib import Telnet from rich.file_proxy import FileProxy from pdbr.helpers import run_ipython_shell def shell(): import getopt _, args = getopt.getopt(sys.argv[1:], "mhc:", ["command="]) if not args: run_ipython_shell() else: from pdbr.__main__ import main main() def telnet(): from pdbr.__main__ import RichPdb pdb_cls = RichPdb() if len(sys.argv) < 3: pdb_cls.error("Usage : pdbr_telnet hostname port") sys.exit() class MyTelnet(Telnet): def fill_rawq(self): """ exactly the same with Telnet.fill_rawq, buffer size is just changed from 50 to 1024. """ if self.irawq >= len(self.rawq): self.rawq = b"" self.irawq = 0 buf = self.sock.recv(1024) self.msg("recv %r", buf) self.eof = not buf self.rawq = self.rawq + buf console = pdb_cls.console sys.stdout = FileProxy(console, sys.stdout) sys.stderr = FileProxy(console, sys.stderr) try: host = sys.argv[1] port = int(sys.argv[2]) with MyTelnet(host, port) as tn: tn.interact() except BaseException as e: pdb_cls.error(e) sys.exit() ================================================ FILE: pdbr/helpers.py ================================================ import sys from pdbr.__main__ import RichPdb def run_ipython_shell(): try: from IPython.terminal.interactiveshell import TerminalInteractiveShell from IPython.terminal.ipapp import TerminalIPythonApp from prompt_toolkit.history import FileHistory from traitlets import Type TerminalInteractiveShell.simple_prompt = False except ModuleNotFoundError as error: raise type(error)( "In order to use pdbr shell, install IPython with pdbr[ipython]" ) from error class PdbrTerminalInteractiveShell(TerminalInteractiveShell): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if RichPdb._ipython_history_file: self.debugger_history = FileHistory(RichPdb._ipython_history_file) @property def debugger_cls(self): return RichPdb class PdbrTerminalIPythonApp(TerminalIPythonApp): interactive_shell_class = Type( klass=object, # use default_value otherwise which only allow subclasses. default_value=PdbrTerminalInteractiveShell, help=( "Class to use to instantiate the TerminalInteractiveShell object. " "Useful for custom Frontends" ), ).tag(config=True) app = PdbrTerminalIPythonApp.instance() app.initialize() sys.exit(app.start()) ================================================ FILE: pdbr/middlewares/__init__.py ================================================ ================================================ FILE: pdbr/middlewares/django.py ================================================ import sys from django.conf import settings from django.core.exceptions import MiddlewareNotUsed from pdbr.__main__ import post_mortem class PdbrMiddleware: def __init__(self, get_response): if not settings.DEBUG: raise MiddlewareNotUsed() self.get_response = get_response def __call__(self, request): return self.get_response(request) def process_exception(self, request, exception): # noqa: F841 post_mortem(sys.exc_info()[2]) ================================================ FILE: pdbr/middlewares/starlette.py ================================================ from starlette.middleware.errors import ServerErrorMiddleware from pdbr._cm import apdbr_context class PdbrMiddleware(ServerErrorMiddleware): async def __call__(self, scope, receive, send) -> None: async with apdbr_context(suppress_exc=False, debug=self.debug): await super().__call__(scope, receive, send) ================================================ FILE: pdbr/runner.py ================================================ import unittest from django.test.runner import DebugSQLTextTestResult, DiscoverRunner from pdbr.__main__ import RichPdb, post_mortem class PDBRDebugResult(unittest.TextTestResult): _pdbr = RichPdb() def addError(self, test, err): super().addError(test, err) self._print(test, err) def addFailure(self, test, err): super().addFailure(test, err) self._print(test, err) def _print(self, test, err): self.buffer = False self._pdbr.message(f"\n{test}") self._pdbr.error("%s: %s", err[0].__name__, err[1]) post_mortem(err[2]) class PdbrDiscoverRunner(DiscoverRunner): def get_resultclass(self): if self.debug_sql: return DebugSQLTextTestResult return PDBRDebugResult ================================================ FILE: pdbr/utils.py ================================================ import atexit import configparser import os from pathlib import Path from pdbr._pdbr import rich_pdb_klass try: import readline except ImportError: try: from pyreadline3 import Readline readline = Readline() except ModuleNotFoundError: readline = None except AttributeError: readline = None def set_history_file(history_file): """ This is just for Pdb, For Ipython, look at RichPdb.pt_init """ if readline is None: return try: readline.read_history_file(history_file) readline.set_history_length(1000) except FileNotFoundError: pass except OSError: pass atexit.register(readline.write_history_file, history_file) def set_traceback(theme): from rich.traceback import install install(theme=theme) def read_config(): style = None theme = None store_history = ".pdbr_history" context = None config = configparser.ConfigParser() config.sections() setup_filename = "setup.cfg" xdg_config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) global_config_path = xdg_config_home / "pdbr" / setup_filename cwd_config_path = Path.cwd() / setup_filename config_path = cwd_config_path.exists() and cwd_config_path or global_config_path config.read(config_path) if "pdbr" in config: if "style" in config["pdbr"]: style = config["pdbr"]["style"] if "theme" in config["pdbr"]: theme = config["pdbr"]["theme"] if "use_traceback" in config["pdbr"]: if config["pdbr"]["use_traceback"].lower() == "true": set_traceback(theme) else: set_traceback(theme) if "store_history" in config["pdbr"]: store_history = config["pdbr"]["store_history"] if "context" in config["pdbr"]: context = config["pdbr"]["context"] history_file = str(Path.home() / store_history) set_history_file(history_file) ipython_history_file = f"{history_file}_ipython" return style, theme, history_file, ipython_history_file, context def debugger_cls( klass=None, console=None, context=None, is_celery=False, show_layouts=True ): if klass is None: try: from IPython.terminal.debugger import TerminalPdb klass = TerminalPdb except ImportError: from pdb import Pdb klass = Pdb style, theme, history_file, ipython_history_file, config_context = read_config() RichPdb = rich_pdb_klass( klass, console=console, context=context if context is not None else config_context, is_celery=is_celery, show_layouts=show_layouts, ) RichPdb._style = style RichPdb._theme = theme RichPdb._history_file = history_file RichPdb._ipython_history_file = ipython_history_file return RichPdb def _pdbr_cls(console=None, context=None, return_instance=True, show_layouts=True): klass = debugger_cls(console=console, context=context, show_layouts=show_layouts) if return_instance: return klass() return klass def _rdbr_cls(return_instance=True): try: from celery.contrib import rdb rdb.BANNER = """\ {self.ident}: Type `pdbr_telnet {self.host} {self.port}` to connect {self.ident}: Waiting for client... """ except ModuleNotFoundError as error: raise type(error)("In order to install celery, use pdbr[celery]") from error klass = debugger_cls(klass=rdb.Rdb, is_celery=True, show_layouts=False) if return_instance: return klass() return klass ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "pdbr" version = "0.9.7" description = "Pdb with Rich library." authors = ["Can Sarigol "] packages = [ { include = "pdbr" } ] readme = "README.md" homepage = "https://github.com/cansarigol/pdbr" repository = "https://github.com/cansarigol/pdbr" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] [tool.poetry.dependencies] python = "^3.7.9" rich = "*" ipython = {version = "*", optional = true} pyreadline3 = {version = "^3.4.1", markers = "sys_platform == 'win32'"} [tool.poetry.extras] ipython = ["ipython"] [tool.poetry.scripts] pdbr = 'pdbr.cli:shell' pdbr_telnet = 'pdbr.cli:telnet' [tool.poetry.group.dev.dependencies] ruff = "^0.6.5" nox = "^2024.4.15" [build-system] requires = ["poetry-core>=1.2.0"] build-backend = "poetry.core.masonry.api" [tool.vulture] make_whitelist = true min_confidence = 80 paths = ["pdbr", "tests"] sort_by_size = true verbose = false [project] name = "pdbr" version = "0.9.7" [tool.setuptools] py-modules = [] [tool.ruff] line-length = 88 [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "C4", # flake8-comprehensions "PIE", # flake8-pie "ERA", # eradicate ] ================================================ FILE: runtests.py ================================================ import os import sys import django from django.conf import settings from django.test.utils import get_runner if __name__ == "__main__": os.environ["DJANGO_SETTINGS_MODULE"] = "tests.tests_django.test_settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) ================================================ FILE: scripts/lint ================================================ #!/bin/sh -e export SOURCE_FILES="pdbr tests noxfile.py" ruff check $SOURCE_FILES --fix black $SOURCE_FILES ================================================ FILE: scripts/test ================================================ #!/bin/sh -e pre-commit run --all-files poetry run nox --sessions test django_test ================================================ FILE: setup.cfg ================================================ [tool:pytest] addopts = --capture=no --disable-warnings [pdbr] use_traceback= True style=dim store_history=.pdbr_history ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/conftest.py ================================================ """ Add '--skip-slow' cmdline option to skip tests that are marked with @pytest.mark.slow. """ import pytest def pytest_addoption(parser): parser.addoption( "--skip-slow", action="store_true", default=False, help="Skip slow tests" ) def pytest_collection_modifyitems(config, items): if not config.getoption("--skip-slow"): return skip_slow = pytest.mark.skip(reason="Specified --skip-slow") for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) ================================================ FILE: tests/test_api.py ================================================ import pdbr def test_api_attr(): assert pdbr.__all__ == [ "set_trace", "run", "pm", "post_mortem", "celery_set_trace", "RichPdb", "pdbr_context", "apdbr_context", ] ================================================ FILE: tests/test_config.py ================================================ import os from pathlib import Path from tempfile import TemporaryDirectory import pytest from pdbr.utils import read_config root_dir = Path(__file__).parents[1] @pytest.fixture def dummy_global_config(): XDG_CONFIG_HOME = Path.home() / ".config" pdbr_dir = XDG_CONFIG_HOME / "pdbr" pdbr_dir.mkdir(exist_ok=True, parents=True) setup_file = pdbr_dir / "setup.cfg" backup_file = pdbr_dir / (setup_file.stem + ".cfg.bak") if setup_file.exists(): setup_file.rename(backup_file) with open(setup_file, "wt") as f: f.writelines(["[pdbr]\n", "theme = ansi_light"]) yield setup_file setup_file.unlink() if backup_file.exists(): backup_file.rename(setup_file) def test_global_config(dummy_global_config): assert dummy_global_config.exists() tmpdir = TemporaryDirectory() os.chdir(tmpdir.name) # Second element of tuple is theme assert read_config()[1] == "ansi_light" os.chdir(root_dir) def test_local_config(): tmpdir = TemporaryDirectory() os.chdir(tmpdir.name) setup_file = Path(tmpdir.name) / "setup.cfg" with open(setup_file, "wt") as f: f.writelines(["[pdbr]\n", "theme = ansi_dark"]) assert read_config()[1] == "ansi_dark" os.chdir(root_dir) def test_read_config(): pdbr_history = str(Path.home() / ".pdbr_history") pdbr_history_ipython = str(Path.home() / ".pdbr_history_ipython") assert read_config() == ("dim", None, pdbr_history, pdbr_history_ipython, None) ================================================ FILE: tests/test_magic.py ================================================ import inspect import re import sys from pathlib import Path import pytest from rich.console import Console from rich.theme import Theme from pdbr._pdbr import rich_pdb_klass NUMBER_RE = r"[\d.e+_,-]+" # Matches 1e+03, 1.0e-03, 1_000, 1,000 TAG_RE = re.compile(r"\x1b[\[\]]+[\dDClhJt;?]+m?") def untag(s): """Not perfect, but does the job. >>> untag('\x1b[0mfoo\x1b[0m\x1b[0;34m(\x1b[0m\x1b[0marg\x1b[0m\x1b[0;34m)\x1b[0m' >>> '\x1b[0;34m\x1b[0m\x1b[0;34m\x1b[0m\x1b[0m') 'foo(arg)' """ s = s.replace("\x07", "") s = s.replace("\x1b[?2004l", "") return TAG_RE.sub("", s) def unquote(s): """ >>> unquote('"foo"') 'foo' >>> unquote('"foo"bar') '"foo"bar' """ for quote in ('"', "'"): if s.startswith(quote) and s.endswith(quote): return s[1:-1] return s TMP_FILE_CONTENT = '''def foo(arg): """Foo docstring""" pass ''' def import_tmp_file(rpdb, tmp_path: Path, file_content=TMP_FILE_CONTENT) -> Path: """Creates a temporary file, writes `file_content` to it and makes pdbr import it""" tmp_file = tmp_path / "foo.py" tmp_file.write_text(file_content) rpdb.precmd(f'import sys; sys.path.append("{tmp_file.parent.absolute()}")') rpdb.precmd(f"from {tmp_file.stem} import foo") return tmp_file @pytest.fixture def pdbr_child_process(tmp_path): """ Spawn a pdbr prompt in a child process. """ from pexpect import spawn file = tmp_path / "foo.py" file.write_text("import pdbr;breakpoint()") child = spawn( str(Path(sys.executable)), [str(file)], encoding="utf-8", ) child.expect("breakpoint") child.timeout = 10 return child @pytest.fixture def RichIPdb(): """ In contrast to the normal RichPdb in test_pdbr.py which inherits from built-in pdb.Pdb, this one inherits from IPython's TerminalPdb, which holds a 'shell' attribute that is a IPython TerminalInteractiveShell. This is required for the magic commands to work (and happens automatically when the user runs pdbr when IPython is importable). """ from IPython.terminal.debugger import TerminalPdb currentframe = inspect.currentframe() def rich_ipdb_klass(*args, **kwargs): ripdb = rich_pdb_klass(TerminalPdb, show_layouts=False)(*args, **kwargs) # Set frame and stack related self-attributes ripdb.botframe = currentframe.f_back ripdb.setup(currentframe.f_back, None) # Set the console's file to stdout so that we can capture the output _console = Console( file=kwargs.get("stdout", sys.stdout), theme=Theme( {"info": "dim cyan", "warning": "magenta", "danger": "bold red"} ), ) ripdb._console = _console return ripdb return rich_ipdb_klass @pytest.mark.skipif(sys.platform.startswith("win"), reason="pexpect") @pytest.mark.slow class TestPdbrChildProcess: def test_time(self, pdbr_child_process): pdbr_child_process.sendline("from time import sleep") pdbr_child_process.sendline("%time sleep(0.1)") pdbr_child_process.expect(re.compile("CPU times: .+")) pdbr_child_process.expect("Wall time: .+") def test_timeit(self, pdbr_child_process): pdbr_child_process.sendline("%timeit -n 1 -r 1 pass") pdbr_child_process.expect_exact("std. dev. of 1 run, 1 loop each)") @pytest.mark.skipif(sys.platform.startswith("win"), reason="pexpect") class TestPdbrMagic: def test_onecmd_time_line_magic(self, capsys, RichIPdb): RichIPdb().precmd("%time pass") captured = capsys.readouterr() output = captured.out assert re.search( f"CPU times: user {NUMBER_RE} [mµn]s, " f"sys: {NUMBER_RE} [mµn]s, " f"total: {NUMBER_RE} [mµn]s\n" f"Wall time: {NUMBER_RE} [mµn]s", output, ) def test_onecmd_unsupported_cell_magic(self, capsys, RichIPdb): RichIPdb().precmd("%%time pass") captured = capsys.readouterr() output = captured.out error = ( "Cell magics (multiline) are not yet supported. Use a single '%' instead." ) assert output == "*** " + error + "\n" cmd = "%%time" stop = RichIPdb().precmd(cmd) captured_output = capsys.readouterr().out assert not stop RichIPdb().error(error) cell_magics_error = capsys.readouterr().out assert cell_magics_error == captured_output def test_onecmd_lsmagic_line_magic(self, capsys, RichIPdb): RichIPdb().precmd("%lsmagic") captured = capsys.readouterr() output = captured.out assert re.search( "Available line magics:\n%alias +%alias_magic +%autoawait.*%%writefile", output, re.DOTALL, ) def test_no_zombie_lastcmd(self, capsys, RichIPdb): rpdb = RichIPdb(stdout=sys.stdout) rpdb.precmd("print('SHOULD_NOT_BE_IN_%pwd_OUTPUT')") captured = capsys.readouterr() assert captured.out.endswith( "SHOULD_NOT_BE_IN_%pwd_OUTPUT\n" ) # Starts with colors and prompt rpdb.precmd("%pwd") captured = capsys.readouterr() assert captured.out.endswith(Path.cwd().absolute().as_posix() + "\n") assert "SHOULD_NOT_BE_IN_%pwd_OUTPUT" not in captured.out def test_IPython_Pdb_magics_implementation(self, tmp_path, capsys, RichIPdb): """ We test do_{magic} methods that are concretely implemented by IPython.core.debugger.Pdb, and don't default to IPython's 'InteractiveShell.run_line_magic()' like the other magics. """ from IPython.utils.text import dedent rpdb = RichIPdb(stdout=sys.stdout) tmp_file = import_tmp_file(rpdb, tmp_path) # pdef rpdb.do_pdef("foo") do_pdef_foo_output = capsys.readouterr().out untagged = untag(do_pdef_foo_output).strip() assert untagged.endswith("foo(arg)"), untagged rpdb.precmd("%pdef foo") magic_pdef_foo_output = capsys.readouterr().out untagged = untag(magic_pdef_foo_output).strip() assert untagged.endswith("foo(arg)"), untagged # pdoc rpdb.precmd("%pdoc foo") magic_pdef_foo_output = capsys.readouterr().out untagged = untag(magic_pdef_foo_output).strip() expected_docstring = dedent( """Class docstring: Foo docstring Call docstring: Call self as a function.""" ) assert untagged == expected_docstring, untagged # pfile rpdb.precmd("%pfile foo") magic_pfile_foo_output = capsys.readouterr().out untagged = untag(magic_pfile_foo_output).strip() tmp_file_content = Path(tmp_file).read_text().strip() assert untagged == tmp_file_content # pinfo rpdb.precmd("%pinfo foo") magic_pinfo_foo_output = capsys.readouterr().out untagged = untag(magic_pinfo_foo_output).strip() expected_pinfo = dedent( f"""Signature: foo(arg) Docstring: Foo docstring File: {tmp_file.absolute()} Type: function""" ) assert untagged == expected_pinfo, untagged # pinfo2 rpdb.precmd("%pinfo2 foo") magic_pinfo2_foo_output = capsys.readouterr().out untagged = untag(magic_pinfo2_foo_output).strip() expected_pinfo2 = re.compile( dedent( rf"""Signature: foo\(arg\) Source:\s* %s File: {tmp_file.absolute()} Type: function""" ) % re.escape(tmp_file_content) ) assert expected_pinfo2.fullmatch(untagged), untagged # psource rpdb.precmd("%psource foo") magic_psource_foo_output = capsys.readouterr().out untagged = untag(magic_psource_foo_output).strip() expected_psource = 'def foo(arg):\n """Foo docstring"""\n pass' assert untagged == expected_psource, untagged def test_expr_questionmark_pinfo(self, tmp_path, capsys, RichIPdb): from IPython.utils.text import dedent rpdb = RichIPdb(stdout=sys.stdout) tmp_file = import_tmp_file(rpdb, tmp_path) # pinfo rpdb.precmd(rpdb.precmd("foo?")) magic_foo_qmark_output = capsys.readouterr().out untagged = untag(magic_foo_qmark_output).strip() expected_pinfo_path = ( f"/private/var/folders/.*/{tmp_file.name}" if sys.platform == "darwin" else f"/tmp/.*/{tmp_file.name}" ) expected_pinfo = re.compile( dedent( rf""".*Signature: foo\(arg\) Docstring: Foo docstring File: {expected_pinfo_path} Type: function""" ) ) assert expected_pinfo.fullmatch(untagged), f"untagged = {untagged!r}" # pinfo2 rpdb.precmd(rpdb.precmd("foo??")) magic_foo_qmark2_output = capsys.readouterr().out rpdb.precmd(rpdb.precmd("%pinfo2 foo")) magic_pinfo2_foo_output = capsys.readouterr().out assert magic_pinfo2_foo_output == magic_foo_qmark2_output def test_filesystem_magics(self, capsys, RichIPdb): cwd = Path.cwd().absolute().as_posix() rpdb = RichIPdb(stdout=sys.stdout) rpdb.precmd("%pwd") pwd_output = capsys.readouterr().out.strip() assert pwd_output == cwd rpdb.precmd("import os; os.getcwd()") pwd_output = unquote(capsys.readouterr().out.strip()) assert pwd_output == cwd new_dir = str(Path.cwd().absolute().parent) rpdb.precmd(f"%cd {new_dir}") cd_output = untag(capsys.readouterr().out.strip()) assert cd_output.endswith(new_dir) rpdb.precmd("%pwd") pwd_output = capsys.readouterr().out.strip() assert pwd_output == new_dir rpdb.precmd("import os; os.getcwd()") pwd_output = unquote(capsys.readouterr().out.strip()) assert pwd_output == new_dir ================================================ FILE: tests/test_pdbr.py ================================================ import inspect import pdb import pytest from pdbr._pdbr import rich_pdb_klass @pytest.fixture def RichPdb(*args, **kwargs): currentframe = inspect.currentframe() def wrapper(): rpdb = rich_pdb_klass(pdb.Pdb, show_layouts=False)(*args, **kwargs) # Set frame and stack related self-attributes rpdb.botframe = currentframe.f_back rpdb.setup(currentframe.f_back, None) return rpdb return wrapper def test_prompt(RichPdb): assert RichPdb().prompt == "(Pdbr) " def test_print(capsys, RichPdb): RichPdb()._print("msg") captured = capsys.readouterr() assert captured.out == "msg\n" def test_print_error(capsys, RichPdb): RichPdb().error("error") captured = capsys.readouterr() assert captured.out == "\x1b[1;31m*** error\x1b[0m\n" def test_print_with_style(capsys, RichPdb): RichPdb()._print("msg", style="yellow") captured = capsys.readouterr() assert captured.out == "\x1b[33mmsg\x1b[0m\n" def test_print_without_escape_tag(capsys, RichPdb): RichPdb()._print("[blue]msg[/]") captured = capsys.readouterr() assert captured.out == "\x1b[34mmsg\x1b[0m\n" def test_print_array(capsys, RichPdb): RichPdb()._print("[[8]]") captured = capsys.readouterr() assert ( captured.out == "\x1b[1m[\x1b[0m\x1b[1m[\x1b[0m\x1b[1;36m8" "\x1b[0m\x1b[1m]\x1b[0m\x1b[1m]\x1b[0m\n" ) def test_onecmd(capsys, RichPdb): rpdb = RichPdb() cmd = 'print("msg")' stop = rpdb.precmd(cmd) captured = capsys.readouterr() assert not stop assert captured.out == "msg\n" ================================================ FILE: tests/tests_django/__init__.py ================================================ ================================================ FILE: tests/tests_django/test_settings.py ================================================ from pathlib import Path BASE_DIR = Path(__file__).absolute().parents[1] SECRET_KEY = "fake-key" DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": str(BASE_DIR / "db.sqlite3"), } } INSTALLED_APPS = ("tests.tests_django",) TEST_RUNNER = "pdbr.runner.PdbrDiscoverRunner" ROOT_URLCONF = "tests.tests_django.urls" MIDDLEWARE = ["pdbr.middlewares.django.PdbrMiddleware"] ================================================ FILE: tests/tests_django/tests.py ================================================ from django.test import TestCase class DjangoTest(TestCase): def test_runner(self): self.assertEqual("foo", "foo") def test_middleware(self): response = self.client.get("") self.assertEqual(response.status_code, 200) ================================================ FILE: tests/tests_django/urls.py ================================================ from django.http import HttpResponse from django.urls import path urlpatterns = [ path("", lambda request: HttpResponse()), ]