Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(debugger): passing debugger to app #3967

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
674b0ab
feat: add customizable debugger module for exception handling
Jan 22, 2025
d666ba6
feat(debug): introduce debugger type protocols for enhanced exception…
Jan 22, 2025
2097919
feat(debug): update debugger module references to use Debugger type
Jan 22, 2025
e5fb4ef
refactor(debug): reorganize imports and improve type aliasing in debu…
Jan 22, 2025
2ce9545
chore(pre-commit): update exclude pattern to include debugging test f…
Jan 22, 2025
36b5ae5
refactor(debug): moved test debug app into a directory
Jan 22, 2025
4cf211a
chore(pre-commit): update exclude patterns for debugging and codespel…
Jan 22, 2025
79ccec4
docs(debug): add section on configuring Litestar with advanced Python…
Jan 22, 2025
0498666
docs(debug): enhance section on configuring Litestar with default deb…
Jan 23, 2025
c9cecdb
docs(debug): fix formatting for default pdb reference in debugging do…
Jan 23, 2025
4d6b8da
fix(debug): include ModuleType in Debugger type alias for enhanced ty…
Jan 23, 2025
b8aec1a
fix(debug): import Debugger type from debugger_types for improved typ…
Jan 23, 2025
14b0de1
fix(debug): update debugger module docstring to clarify support for p…
Jan 23, 2025
7f0b722
fix(debug): update debugger_module docstring to clarify support for p…
Jan 23, 2025
5f86bfe
fix(debug): remove redundant Debugger import from type checking and u…
Jan 23, 2025
a355607
fix(debug): remove unnecessary whitespace in zero_division_error handler
Jan 23, 2025
a743e02
fix(tests): update post_mortem patch import in exception handler midd…
Jan 23, 2025
c0e52be
fix(config): update codespell exclusions and ignore-words-list in con…
Jan 23, 2025
0145910
fix(docs): update debugging.rst to reflect changes in Litestar app co…
Jan 27, 2025
fee80dc
docs(debug): changed the header of usage doc
oek1ng Jan 27, 2025
0dfd519
refactor(debugger): simplify PDBProtocol and remove unused protocol c…
Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ repos:
- id: check-case-conflict
- id: check-toml
- id: debug-statements
exclude: "litestar/middleware/_internal/exceptions/middleware.py"
exclude: ^(litestar/config/app\.py|litestar/app\.py|test_apps/debugging/main\.py)$
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
Expand Down
15 changes: 15 additions & 0 deletions docs/usage/debugging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,18 @@ Using uvicorn
5. Run your application via the debugger via ``Run`` > ``Start debugging``
.. image:: /images/debugging/vs-code-debug.png
:align: center


Customizing the debugger
-------------------------

You can configure Litestar with the debug_module option to enable interactive debugging. Currently, it supports the following debugging tools:
`ipdb <https://github.com/gotcha/ipdb>`_, `PuDB <https://documen.tician.de/pudb/>`_ and `pdbr <https://github.com/cansarigol/pdbr>`_. Also supports `pdb++ <https://github.com/pdbpp/pdbpp>`_.
The default value is `pdb <https://docs.python.org/3/library/pdb.html>`_.

.. code-block:: python

import ipdb


app = Litestar(pdb_on_exception=True, debugger_module=ipdb)
8 changes: 8 additions & 0 deletions litestar/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import inspect
import logging
import os
import pdb # noqa: T100
import warnings
from contextlib import (
AbstractAsyncContextManager,
Expand Down Expand Up @@ -79,6 +80,7 @@
BeforeMessageSendHookHandler,
BeforeRequestHookHandler,
ControllerRouterHandler,
Debugger,
Dependencies,
EmptyType,
ExceptionHandlersMap,
Expand Down Expand Up @@ -149,6 +151,7 @@ class Litestar(Router):
"compression_config",
"cors_config",
"csrf_config",
"debugger_module",
"event_emitter",
"experimental_features",
"get_logger",
Expand Down Expand Up @@ -222,6 +225,7 @@ def __init__(
lifespan: Sequence[Callable[[Litestar], AbstractAsyncContextManager] | AbstractAsyncContextManager]
| None = None,
pdb_on_exception: bool | None = None,
debugger_module: Debugger = pdb,
experimental_features: Iterable[ExperimentalFeatures] | None = None,
) -> None:
"""Initialize a ``Litestar`` application.
Expand Down Expand Up @@ -285,6 +289,8 @@ def __init__(

.. versionadded:: 2.8.0
pdb_on_exception: Drop into the PDB when an exception occurs.
debugger_module: A `pdb`-like debugger module that supports the `post_mortem()` protocol.
This module will be used when `pdb_on_exception` is set to True.
plugins: Sequence of plugins.
request_class: An optional subclass of :class:`Request <.connection.Request>` to use for http connections.
request_max_body_size: Maximum allowed size of the request body in bytes. If this size is exceeded, a
Expand Down Expand Up @@ -362,6 +368,7 @@ def __init__(
path=path or "",
parameters=parameters or {},
pdb_on_exception=pdb_on_exception,
debugger_module=debugger_module,
plugins=self._get_default_plugins(list(plugins or [])),
request_class=request_class,
request_max_body_size=request_max_body_size,
Expand Down Expand Up @@ -440,6 +447,7 @@ def __init__(
self.websocket_class: type[WebSocket] = config.websocket_class or WebSocket
self.debug = config.debug
self.pdb_on_exception: bool = config.pdb_on_exception
self.debugger_module: Debugger = config.debugger_module
self.include_in_schema = include_in_schema

if self.pdb_on_exception:
Expand Down
5 changes: 5 additions & 0 deletions litestar/config/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import enum
import pdb # noqa: T100
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable

Expand Down Expand Up @@ -38,6 +39,7 @@
BeforeMessageSendHookHandler,
BeforeRequestHookHandler,
ControllerRouterHandler,
Debugger,
ExceptionHandlersMap,
Guard,
Middleware,
Expand Down Expand Up @@ -159,6 +161,9 @@ class AppConfig:
"""
pdb_on_exception: bool = field(default=False)
"""Drop into the PDB on an exception"""
debugger_module: Debugger = field(default=pdb)
"""A `pdb`-like debugger module that supports the `post_mortem()` protocol.
This module will be used when `pdb_on_exception` is set to True."""
plugins: list[PluginProtocol] = field(default_factory=list)
"""List of :class:`SerializationPluginProtocol <.plugins.SerializationPluginProtocol>`."""
request_class: type[Request] | None = field(default=None)
Expand Down
3 changes: 1 addition & 2 deletions litestar/middleware/_internal/exceptions/middleware.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import pdb # noqa: T100
from inspect import getmro
from sys import exc_info
from traceback import format_exception
Expand Down Expand Up @@ -170,7 +169,7 @@ async def capture_response_started(event: Message) -> None:
await hook(e, scope)

if litestar_app.pdb_on_exception:
pdb.post_mortem()
litestar_app.debugger_module.post_mortem()

if scope["type"] == ScopeType.HTTP:
await self.handle_request_exception(
Expand Down
2 changes: 2 additions & 0 deletions litestar/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
TypeDecodersSequence,
TypeEncodersMap,
)
from .debugger_types import Debugger
from .empty import Empty, EmptyType
from .file_types import FileInfo, FileSystemProtocol
from .helper_types import AnyIOBackend, MaybePartial, OptionalSequence, SSEData, StreamType, SyncOrAsyncUnion
Expand All @@ -95,6 +96,7 @@
"ControllerRouterHandler",
"DataContainerType",
"DataclassProtocol",
"Debugger",
"Dependencies",
"Empty",
"EmptyType",
Expand Down
16 changes: 16 additions & 0 deletions litestar/types/debugger_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from types import ModuleType, TracebackType
from typing import Any, Optional, Protocol, Union

from typing_extensions import TypeAlias


class PDBProtocol(Protocol):
@staticmethod
def post_mortem(
traceback: Optional[TracebackType] = None,
*args: Any,
**kwargs: Any,
) -> Any: ...


Debugger: TypeAlias = Union[ModuleType, PDBProtocol]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ build-backend = "hatchling.build"
requires = ["hatchling"]

[tool.codespell]
ignore-words-list = "selectin"
ignore-words-list = "selectin, documen"
skip = 'uv.lock,docs/examples/contrib/sqlalchemy/us_state_lookup.json'

[tool.coverage.run]
Expand Down
Empty file.
44 changes: 44 additions & 0 deletions test_apps/debugging/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import asyncio
import pdb # noqa: T100
from concurrent.futures import ThreadPoolExecutor
from typing import Dict

# Install this packages if you want to run this test-app
import ipdb # pyright: ignore # noqa: T100
import pdbr # pyright: ignore
import pudb # pyright: ignore # noqa: T100
import uvicorn

from litestar import Litestar, get


@get("/")
async def zero_division_error() -> Dict[str, str]:
"""Handler function that returns a greeting dictionary."""
1 / 0 # pyright: ignore # noqa: B018
return {"message": "ZeroDevisionError didn't occur."}


pdb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pdb)
ipdb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=ipdb)
pudb_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pudb)
pdbr_app = Litestar(route_handlers=[zero_division_error], pdb_on_exception=True, debugger_module=pdbr)


def run_server(app, port):
uvicorn.run(app, port=port)


async def start_servers():
with ThreadPoolExecutor() as executor:
# Run all the servers concurrently
await asyncio.gather(
asyncio.get_event_loop().run_in_executor(executor, run_server, pdb_app, 8000),
asyncio.get_event_loop().run_in_executor(executor, run_server, ipdb_app, 8001),
asyncio.get_event_loop().run_in_executor(executor, run_server, pudb_app, 8002),
asyncio.get_event_loop().run_in_executor(executor, run_server, pdbr_app, 8003),
)


if __name__ == "__main__":
asyncio.run(start_servers())
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def test_pdb_on_exception(mocker: MockerFixture) -> None:
def handler() -> None:
raise ValueError("Test debug exception")

mock_post_mortem = mocker.patch("litestar.middleware._internal.exceptions.middleware.pdb.post_mortem")
mock_post_mortem = mocker.patch("litestar.app.pdb.post_mortem")

app = Litestar([handler], pdb_on_exception=True)

Expand Down
Loading