Repository: litestar-org/litestar

Issue:
Enhancement: Python 3.12
### Summary

3.12.0b4 is underway and the release date for 3.12 quickly approaches.

Release Schedule: https://peps.python.org/pep-0693/
Available Betas/RCs: https://www.python.org/ftp/python/3.12.0/

Tooling is beginning to support this (https://github.com/astral-sh/ruff/commit/8bc7378002d080094d572e83f3a3a69df1023030), I imagine we will have a waiting game for some things.. but it would be nice to get a head start on this once 2.0 is released.

Support 3.12, adding it to our CI matrix version, testing, docs, etc.

## Pre-3.12 Release Pending Upstream Packages

- [x] `greenlet`
  - [x] https://github.com/python-greenlet/greenlet/pull/327
  - [x] Greenlet 3.0 release
- [x] `grpcio`
	- [x] https://github.com/grpc/grpc/issues/33063
- [x] `orjson >= 3.8.10`
    - [x] https://github.com/ijl/orjson/releases/tag/3.8.10

## Post-3.12 Release Pending Upstream Packages

- [x] https://github.com/MagicStack/uvloop/issues/567

 
--- 

<!-- POLAR PLEDGE BADGE START -->
<a href="https://polar.sh/litestar-org/litestar/issues/1862">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/1862/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/1862/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:
added greenlet, grpcio, and orjson deps.
Why orjson?
piccolo

Resolving patch:
diff --git a/litestar/logging/config.py b/litestar/logging/config.py
--- a/litestar/logging/config.py
+++ b/litestar/logging/config.py
@@ -40,6 +40,7 @@
         "class": "litestar.logging.standard.QueueListenerHandler",
         "level": "DEBUG",
         "formatter": "standard",
+        "handlers": ["console"],
     },
 }
 
@@ -327,7 +328,6 @@ def configure(self) -> GetLogger:
 
         from structlog import configure, get_logger
 
-        # we now configure structlog
         configure(
             **{
                 k: v
diff --git a/litestar/logging/standard.py b/litestar/logging/standard.py
--- a/litestar/logging/standard.py
+++ b/litestar/logging/standard.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import atexit
+import sys
 from logging import StreamHandler
 from logging.handlers import QueueHandler, QueueListener
 from queue import Queue
@@ -11,18 +12,23 @@
 __all__ = ("QueueListenerHandler",)
 
 
-class QueueListenerHandler(QueueHandler):
-    """Configure queue listener and handler to support non-blocking logging configuration."""
+if sys.version_info < (3, 12):
 
-    def __init__(self, handlers: list[Any] | None = None) -> None:
-        """Initialize `?QueueListenerHandler`.
+    class QueueListenerHandler(QueueHandler):
+        """Configure queue listener and handler to support non-blocking logging configuration."""
 
-        Args:
-            handlers: Optional 'ConvertingList'
-        """
-        super().__init__(Queue(-1))
-        handlers = resolve_handlers(handlers) if handlers else [StreamHandler()]
-        self.listener = QueueListener(self.queue, *handlers)
-        self.listener.start()
+        def __init__(self, handlers: list[Any] | None = None) -> None:
+            """Initialize `?QueueListenerHandler`.
 
-        atexit.register(self.listener.stop)
+            Args:
+                handlers: Optional 'ConvertingList'
+            """
+            super().__init__(Queue(-1))
+            handlers = resolve_handlers(handlers) if handlers else [StreamHandler()]
+            self.listener = QueueListener(self.queue, *handlers)
+            self.listener.start()
+
+            atexit.register(self.listener.stop)
+
+else:
+    QueueListenerHandler = QueueHandler
diff --git a/litestar/middleware/logging.py b/litestar/middleware/logging.py
--- a/litestar/middleware/logging.py
+++ b/litestar/middleware/logging.py
@@ -106,7 +106,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         """
         if not hasattr(self, "logger"):
             self.logger = scope["app"].get_logger(self.config.logger_name)
-            self.is_struct_logger = structlog_installed and isinstance(self.logger, BindableLogger)
+            self.is_struct_logger = structlog_installed and repr(self.logger).startswith("<BoundLoggerLazyProxy")
 
         if self.config.response_log_fields:
             send = self.create_send_wrapper(scope=scope, send=send)


Test patch:
diff --git a/tests/unit/test_cli/test_session_commands.py b/tests/unit/test_cli/test_session_commands.py
--- a/tests/unit/test_cli/test_session_commands.py
+++ b/tests/unit/test_cli/test_session_commands.py
@@ -48,7 +48,7 @@ def test_delete_session(
 
     result = runner.invoke(cli_command, ["sessions", "delete", "foo"])
 
-    assert mock_confirm_ask.called_once_with("[red]Delete session 'foo'?")
+    mock_confirm_ask.assert_called_once_with("Delete session 'foo'?")
     assert not result.exception
     mock_delete.assert_called_once_with("foo")
 
@@ -78,6 +78,6 @@ def test_clear_sessions(
 
     result = runner.invoke(cli_command, ["sessions", "clear"])
 
-    assert mock_confirm_ask.called_once_with("[red]Delete all sessions?")
+    mock_confirm_ask.assert_called_once_with("[red]Delete all sessions?")
     assert not result.exception
     mock_delete.assert_called_once()
diff --git a/tests/unit/test_logging/test_logging_config.py b/tests/unit/test_logging/test_logging_config.py
--- a/tests/unit/test_logging/test_logging_config.py
+++ b/tests/unit/test_logging/test_logging_config.py
@@ -1,4 +1,5 @@
 import logging
+import sys
 from typing import TYPE_CHECKING, Any, Dict
 from unittest.mock import Mock, patch
 
@@ -144,12 +145,18 @@ def test_root_logger(handlers: Any, listener: Any) -> None:
 @pytest.mark.parametrize(
     "handlers, listener",
     [
-        [default_handlers, StandardQueueListenerHandler],
+        pytest.param(
+            default_handlers,
+            StandardQueueListenerHandler,
+            marks=pytest.mark.xfail(
+                condition=sys.version_info >= (3, 12), reason="change to QueueHandler/QueueListener config in 3.12"
+            ),
+        ),
         [default_picologging_handlers, PicologgingQueueListenerHandler],
     ],
 )
-def test_customizing_handler(handlers: Any, listener: Any) -> None:
-    handlers["queue_listener"]["handlers"] = ["cfg://handlers.console"]
+def test_customizing_handler(handlers: Any, listener: Any, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setitem(handlers["queue_listener"], "handlers", ["cfg://handlers.console"])
 
     logging_config = LoggingConfig(handlers=handlers)
     get_logger = logging_config.configure()
diff --git a/tests/unit/test_logging/test_structlog_config.py b/tests/unit/test_logging/test_structlog_config.py
--- a/tests/unit/test_logging/test_structlog_config.py
+++ b/tests/unit/test_logging/test_structlog_config.py
@@ -13,8 +13,8 @@
 def test_structlog_config_default(capsys: CaptureFixture) -> None:
     with create_test_client([], logging_config=StructLoggingConfig()) as client:
         assert client.app.logger
-        assert isinstance(client.app.logger, BindableLogger)
-        client.app.logger.info("message", key="value")  # type: ignore [attr-defined]
+        assert isinstance(client.app.logger.bind(), BindableLogger)
+        client.app.logger.info("message", key="value")
 
         log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()]
         assert len(log_messages) == 1
@@ -29,11 +29,11 @@ def test_structlog_config_specify_processors(capsys: CaptureFixture) -> None:
 
     with create_test_client([], logging_config=logging_config) as client:
         assert client.app.logger
-        assert isinstance(client.app.logger, BindableLogger)
+        assert isinstance(client.app.logger.bind(), BindableLogger)
 
-        client.app.logger.info("message1", key="value1")  # type: ignore [attr-defined]
+        client.app.logger.info("message1", key="value1")
         # Log twice to make sure issue #882 doesn't appear again
-        client.app.logger.info("message2", key="value2")  # type: ignore [attr-defined]
+        client.app.logger.info("message2", key="value2")
 
         log_messages = [decode_json(value=x) for x in capsys.readouterr().out.splitlines()]