diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dc2033792..4eec7253d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,8 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" + allow-prereleases: true - name: Install Pre-Commit run: python -m pip install pre-commit && pre-commit install @@ -35,11 +36,11 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] pydantic-version: ["1", "2"] uses: ./.github/workflows/test.yml with: - coverage: ${{ matrix.python-version == '3.11' && matrix.pydantic-version == '2' }} + coverage: ${{ matrix.python-version == '3.12' && matrix.pydantic-version == '2' }} pydantic-version: ${{ matrix.pydantic-version }} python-version: ${{ matrix.python-version }} @@ -51,7 +52,7 @@ jobs: os: ["macos-latest", "windows-latest"] uses: ./.github/workflows/test.yml with: - python-version: "3.11" + python-version: "3.12" pydantic-version: "2" os: ${{ matrix.os }} @@ -109,12 +110,13 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" + allow-prereleases: true - uses: pdm-project/setup-pdm@v3 name: Set up PDM with: - python-version: "3.11" + python-version: "3.12" allow-python-prereleases: false cache: true cache-dependency-path: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7798d276c8..afb4003b6b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - uses: pdm-project/setup-pdm@v3 name: Set up PDM diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0068e8e8d4..7bea786888 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ jobs: name: Test Minimal Application with Base Dependencies runs-on: ubuntu-latest env: - python_version: "3.11" + python_version: "3.12" steps: - name: Check out repository uses: actions/checkout@v4 @@ -18,12 +18,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - uses: pdm-project/setup-pdm@v3 name: Set up PDM with: - python-version: "3.11" + python-version: "3.12" allow-python-prereleases: false cache: true cache-dependency-path: | @@ -50,13 +50,12 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - uses: pdm-project/setup-pdm@v3 name: Set up PDM with: - python-version: "3.11" - allow-python-prereleases: false + python-version: "3.12" cache: true - name: Build package diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b43a4a8d8..fbf2da8996 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: "3.11" + python: "3.12" repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v2.4.0 diff --git a/litestar/logging/config.py b/litestar/logging/config.py index dcf3ea3026..b986773c98 100644 --- 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 index 9140325a24..467b2f1215 100644 --- 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 index d7f0374397..0779830e7b 100644 --- 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("=0.18.0"] cryptography = ["cryptography"] full = [ "litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog]", @@ -88,7 +89,7 @@ prometheus = ["prometheus-client"] pydantic = ["pydantic[email]", "pydantic-extra-types"] redis = ["redis[hiredis]>=4.4.4"] sqlalchemy = ["advanced-alchemy>=0.2.2,<0.4.0"] -standard = ["jinja2", "jsbeautifier", "uvicorn[standard]", "fast-query-parsers>=1.0.2"] +standard = ["jinja2", "jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0", "fast-query-parsers>=1.0.2"] structlog = ["structlog"] [tool.pdm.dev-dependencies] diff --git a/sonar-project.properties b/sonar-project.properties index 9933219670..49fa91b5ee 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.organization=litestar-api sonar.projectKey=litestar-org_litestar sonar.python.coverage.reportPaths=coverage.xml -sonar.python.version=3.8, 3.9, 3.10, 3.11 +sonar.python.version=3.8, 3.9, 3.10, 3.11, 3.12 sonar.sourceEncoding=UTF-8 sonar.sources=litestar sonar.tests=tests diff --git a/tests/unit/test_cli/test_session_commands.py b/tests/unit/test_cli/test_session_commands.py index 7c32f0b3bb..4841c50473 100644 --- 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 index 492e0ebece..a9fcfcddf7 100644 --- 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 index 3240b280bd..4983a8f92b 100644 --- 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()]