Repository: litestar-org/litestar

Issue:
Bug: 2.0.0rc1: `--debug` CLI flag not respected
### Description

Running with --debug does not work. Running with Litestart(..., debug=True) does.

### URL to code causing the issue

https://github.com/Mattwmaster58/mac-bid-utils

### MCVE

```python
from litestar import Litestar, get


@get("/")
async def perch(should_raise_error_that_seen_in_debug: bool) -> str:
    return "helo"


app = Litestar(route_handlers=[perch])
```


### Steps to reproduce


1. Run with `litestar run --debug`
2. Go to root route to trigger what should be an error that's printed with a traceback to the console.
3. Instead, only a single line is shown

I am having this issue in my own app and the issue disappears (ie, logs properly shown) when I comment large chunks of arguments to the Litestar instance that seem completely unrelated, I will investigate further. As of d3318ae on my repo, --debug IS working.



### Screenshots

_No response_

### Logs

```bash
(.venv) ➜  delete_me_litestar_debug_repro litestar run -p 3214 --debug
Using Litestar app from app:app
┌──────────────────────────────┬──────────────────────┐
│ Litestar version             │ 2.0.0                │
│ Debug mode                   │ Enabled              │
│ Python Debugger on exception │ Disabled             │
│ CORS                         │ Disabled             │
│ CSRF                         │ Disabled             │
│ OpenAPI                      │ Enabled path=/schema │
│ Compression                  │ Disabled             │
└──────────────────────────────┴──────────────────────┘
INFO:     Started server process [387087]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:3214 (Press CTRL+C to quit)
INFO:     127.0.0.1:47810 - "GET / HTTP/1.1" 400 Bad Request
INFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [387087]
```


### Litestar Version

2.0.0rc1

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

#### Other

Some discussion on discord about this: https://discord.com/channels/919193495116337154/1138671763190972416

Per some discussion in the thread, this seems to be a separate issue than #1804 

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

<a href="https://polar.sh/litestar-org/litestar/issues/2147">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2147/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2147/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:

I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/cli/commands/core.py b/litestar/cli/commands/core.py
--- a/litestar/cli/commands/core.py
+++ b/litestar/cli/commands/core.py
@@ -10,7 +10,7 @@
 import uvicorn
 from rich.tree import Tree
 
-from litestar.cli._utils import RICH_CLICK_INSTALLED, console, show_app_info
+from litestar.cli._utils import RICH_CLICK_INSTALLED, LitestarEnv, console, show_app_info
 from litestar.routes import HTTPRoute, WebSocketRoute
 from litestar.utils.helpers import unwrap_partial
 
@@ -118,7 +118,7 @@ def run_command(
         if pdb:
             ctx.obj.app.pdb_on_exception = True
 
-    env = ctx.obj
+    env: LitestarEnv = ctx.obj
     app = env.app
 
     reload_dirs = env.reload_dirs or reload_dir
diff --git a/litestar/middleware/exceptions/middleware.py b/litestar/middleware/exceptions/middleware.py
--- a/litestar/middleware/exceptions/middleware.py
+++ b/litestar/middleware/exceptions/middleware.py
@@ -15,6 +15,7 @@
 from litestar.middleware.exceptions._debug_response import create_debug_response
 from litestar.serialization import encode_json
 from litestar.status_codes import HTTP_500_INTERNAL_SERVER_ERROR
+from litestar.utils.deprecation import warn_deprecation
 
 __all__ = ("ExceptionHandlerMiddleware", "ExceptionResponseContent", "create_exception_response")
 
@@ -147,17 +148,33 @@ class ExceptionHandlerMiddleware:
     This used in multiple layers of Litestar.
     """
 
-    def __init__(self, app: ASGIApp, debug: bool, exception_handlers: ExceptionHandlersMap) -> None:
+    def __init__(self, app: ASGIApp, debug: bool | None, exception_handlers: ExceptionHandlersMap) -> None:
         """Initialize ``ExceptionHandlerMiddleware``.
 
         Args:
             app: The ``next`` ASGI app to call.
-            debug: Whether ``debug`` mode is enabled
+            debug: Whether ``debug`` mode is enabled. Deprecated. Debug mode will be inferred from the request scope
             exception_handlers: A dictionary mapping status codes and/or exception types to handler functions.
+
+        .. deprecated:: 2.0.0
+            The ``debug`` parameter is deprecated. It will be inferred from the request scope
         """
         self.app = app
         self.exception_handlers = exception_handlers
         self.debug = debug
+        if debug is not None:
+            warn_deprecation(
+                "2.0.0",
+                deprecated_name="debug",
+                kind="parameter",
+                info="Debug mode will be inferred from the request scope",
+            )
+
+        self._get_debug = self._get_debug_scope if debug is None else lambda *a: debug
+
+    @staticmethod
+    def _get_debug_scope(scope: Scope) -> bool:
+        return scope["app"].debug
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         """ASGI-callable.
@@ -249,7 +266,7 @@ def default_http_exception_handler(self, request: Request, exc: Exception) -> Re
             An HTTP response.
         """
         status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
-        if status_code == HTTP_500_INTERNAL_SERVER_ERROR and self.debug:
+        if status_code == HTTP_500_INTERNAL_SERVER_ERROR and self._get_debug_scope(request.scope):
             return create_debug_response(request=request, exc=exc)
         return create_exception_response(request=request, exc=exc)
 
@@ -265,6 +282,7 @@ def handle_exception_logging(self, logger: Logger, logging_config: BaseLoggingCo
             None
         """
         if (
-            logging_config.log_exceptions == "always" or (logging_config.log_exceptions == "debug" and self.debug)
+            logging_config.log_exceptions == "always"
+            or (logging_config.log_exceptions == "debug" and self._get_debug_scope(scope))
         ) and logging_config.exception_logging_handler:
             logging_config.exception_logging_handler(logger, scope, format_exception(*exc_info()))


Test patch:
diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py
--- a/tests/unit/test_middleware/test_exception_handler_middleware.py
+++ b/tests/unit/test_middleware/test_exception_handler_middleware.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Any, Optional
+from typing import TYPE_CHECKING, Any, Callable, Optional
 
 import pytest
 from _pytest.capture import CaptureFixture
@@ -7,17 +7,14 @@
 from structlog.testing import capture_logs
 
 from litestar import Litestar, Request, Response, get
-from litestar.exceptions import (
-    HTTPException,
-    InternalServerException,
-    ValidationException,
-)
+from litestar.exceptions import HTTPException, InternalServerException, ValidationException
 from litestar.logging.config import LoggingConfig, StructLoggingConfig
 from litestar.middleware.exceptions import ExceptionHandlerMiddleware
 from litestar.middleware.exceptions.middleware import get_exception_handler
 from litestar.status_codes import HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
 from litestar.testing import TestClient, create_test_client
 from litestar.types import ExceptionHandlersMap
+from litestar.types.asgi_types import HTTPScope
 
 if TYPE_CHECKING:
     from _pytest.logging import LogCaptureFixture
@@ -30,13 +27,26 @@ async def dummy_app(scope: Any, receive: Any, send: Any) -> None:
     return None
 
 
-middleware = ExceptionHandlerMiddleware(dummy_app, False, {})
+@pytest.fixture()
+def app() -> Litestar:
+    return Litestar()
+
+
+@pytest.fixture()
+def middleware() -> ExceptionHandlerMiddleware:
+    return ExceptionHandlerMiddleware(dummy_app, False, {})
+
+
+@pytest.fixture()
+def scope(create_scope: Callable[..., HTTPScope], app: Litestar) -> HTTPScope:
+    return create_scope(app=app)
 
 
-def test_default_handle_http_exception_handling_extra_object() -> None:
+def test_default_handle_http_exception_handling_extra_object(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
     response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}),  # type: ignore
-        HTTPException(detail="litestar_exception", extra={"key": "value"}),
+        Request(scope=scope), HTTPException(detail="litestar_exception", extra={"key": "value"})
     )
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     assert response.content == {
@@ -46,28 +56,31 @@ def test_default_handle_http_exception_handling_extra_object() -> None:
     }
 
 
-def test_default_handle_http_exception_handling_extra_none() -> None:
+def test_default_handle_http_exception_handling_extra_none(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
     response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}),  # type: ignore
-        HTTPException(detail="litestar_exception"),
+        Request(scope=scope), HTTPException(detail="litestar_exception")
     )
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     assert response.content == {"detail": "Internal Server Error", "status_code": 500}
 
 
-def test_default_handle_litestar_http_exception_handling() -> None:
+def test_default_handle_litestar_http_exception_handling(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
     response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}),  # type: ignore
-        HTTPException(detail="litestar_exception"),
+        Request(scope=scope), HTTPException(detail="litestar_exception")
     )
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     assert response.content == {"detail": "Internal Server Error", "status_code": 500}
 
 
-def test_default_handle_litestar_http_exception_extra_list() -> None:
+def test_default_handle_litestar_http_exception_extra_list(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
     response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}),  # type: ignore
-        HTTPException(detail="litestar_exception", extra=["extra-1", "extra-2"]),
+        Request(scope=scope), HTTPException(detail="litestar_exception", extra=["extra-1", "extra-2"])
     )
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     assert response.content == {
@@ -77,22 +90,21 @@ def test_default_handle_litestar_http_exception_extra_list() -> None:
     }
 
 
-def test_default_handle_starlette_http_exception_handling() -> None:
+def test_default_handle_starlette_http_exception_handling(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
     response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}),  # type: ignore
+        Request(scope=scope),
         StarletteHTTPException(detail="litestar_exception", status_code=HTTP_500_INTERNAL_SERVER_ERROR),
     )
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
-    assert response.content == {
-        "detail": "Internal Server Error",
-        "status_code": 500,
-    }
+    assert response.content == {"detail": "Internal Server Error", "status_code": 500}
 
 
-def test_default_handle_python_http_exception_handling() -> None:
-    response = middleware.default_http_exception_handler(
-        Request(scope={"type": "http", "method": "GET"}), AttributeError("oops")  # type: ignore
-    )
+def test_default_handle_python_http_exception_handling(
+    scope: HTTPScope, middleware: ExceptionHandlerMiddleware
+) -> None:
+    response = middleware.default_http_exception_handler(Request(scope=scope), AttributeError("oops"))
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     assert response.content == {
         "detail": "Internal Server Error",
@@ -307,3 +319,24 @@ def handler() -> None:
 
     assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
     mock_post_mortem.assert_called_once()
+
+
+def test_get_debug_from_scope(get_logger: "GetLogger", caplog: "LogCaptureFixture") -> None:
+    @get("/test")
+    def handler() -> None:
+        raise ValueError("Test debug exception")
+
+    app = Litestar([handler], debug=False)
+    app.debug = True
+
+    with caplog.at_level("ERROR", "litestar"), TestClient(app=app) as client:
+        client.app.logger = get_logger("litestar")
+        response = client.get("/test")
+
+        assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
+        assert "Test debug exception" in response.text
+        assert len(caplog.records) == 1
+        assert caplog.records[0].levelname == "ERROR"
+        assert caplog.records[0].message.startswith(
+            "exception raised on http connection to route /test\n\nTraceback (most recent call last):\n"
+        )