diff --git a/.all-contributorsrc b/.all-contributorsrc
index dfc3870821..57aba010fc 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1755,6 +1755,15 @@
"contributions": [
"doc"
]
+ },
+ {
+ "login": "jderrien",
+ "name": "jderrien",
+ "avatar_url": "https://avatars.githubusercontent.com/u/145396?v=4",
+ "profile": "https://github.com/jderrien",
+ "contributions": [
+ "doc"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml
index c8be7035f9..067acffe57 100644
--- a/.github/workflows/docs-preview.yml
+++ b/.github/workflows/docs-preview.yml
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@v4
- name: Download artifact
- uses: dawidd6/action-download-artifact@v3
+ uses: dawidd6/action-download-artifact@v5
with:
workflow_conclusion: success
run_id: ${{ github.event.workflow_run.id }}
diff --git a/README.md b/README.md
index 4848d7571a..77f36e736e 100644
--- a/README.md
+++ b/README.md
@@ -563,6 +563,7 @@ see [the contribution guide](CONTRIBUTING.rst).
Joren Six 📖 |
+ jderrien 📖 |
diff --git a/docs/examples/caching/__init__.py b/docs/examples/caching/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/docs/examples/caching/cache.py b/docs/examples/caching/cache.py
new file mode 100644
index 0000000000..deec9eaea1
--- /dev/null
+++ b/docs/examples/caching/cache.py
@@ -0,0 +1,22 @@
+from litestar import Litestar, get
+from litestar.config.response_cache import CACHE_FOREVER
+
+
+@get("/cached", cache=True)
+async def my_cached_handler() -> str:
+ return "cached"
+
+
+@get("/cached-seconds", cache=120) # seconds
+async def my_cached_handler_seconds() -> str:
+ return "cached for 120 seconds"
+
+
+@get("/cached-forever", cache=CACHE_FOREVER)
+async def my_cached_handler_forever() -> str:
+ return "cached forever"
+
+
+app = Litestar(
+ [my_cached_handler, my_cached_handler_seconds, my_cached_handler_forever],
+)
diff --git a/docs/examples/caching/key_builder.py b/docs/examples/caching/key_builder.py
new file mode 100644
index 0000000000..762fb27476
--- /dev/null
+++ b/docs/examples/caching/key_builder.py
@@ -0,0 +1,9 @@
+from litestar import Litestar, Request
+from litestar.config.response_cache import ResponseCacheConfig
+
+
+def key_builder(request: Request) -> str:
+ return request.url.path + request.headers.get("my-header", "")
+
+
+app = Litestar([], response_cache_config=ResponseCacheConfig(key_builder=key_builder))
diff --git a/docs/examples/caching/key_builder_for_route_handler.py b/docs/examples/caching/key_builder_for_route_handler.py
new file mode 100644
index 0000000000..5b4c4d1cb2
--- /dev/null
+++ b/docs/examples/caching/key_builder_for_route_handler.py
@@ -0,0 +1,13 @@
+from litestar import Litestar, Request, get
+
+
+def key_builder(request: Request) -> str:
+ return request.url.path + request.headers.get("my-header", "")
+
+
+@get("/cached-path", cache=True, cache_key_builder=key_builder)
+async def cached_handler() -> str:
+ return "cached"
+
+
+app = Litestar([cached_handler])
diff --git a/docs/examples/caching/redis_store.py b/docs/examples/caching/redis_store.py
new file mode 100644
index 0000000000..8832aab553
--- /dev/null
+++ b/docs/examples/caching/redis_store.py
@@ -0,0 +1,20 @@
+import asyncio
+
+from litestar import Litestar, get
+from litestar.config.response_cache import ResponseCacheConfig
+from litestar.stores.redis import RedisStore
+
+
+@get(cache=10)
+async def something() -> str:
+ await asyncio.sleep(1)
+ return "something"
+
+
+redis_store = RedisStore.with_client(url="redis://localhost/", port=6379, db=0)
+cache_config = ResponseCacheConfig(store="redis_backed_store")
+app = Litestar(
+ [something],
+ stores={"redis_backed_store": redis_store},
+ response_cache_config=cache_config,
+)
diff --git a/docs/examples/plugins/flash_messages/jinja.py b/docs/examples/plugins/flash_messages/jinja.py
index 6772697642..27721ddfa5 100644
--- a/docs/examples/plugins/flash_messages/jinja.py
+++ b/docs/examples/plugins/flash_messages/jinja.py
@@ -1,9 +1,13 @@
from litestar import Litestar
from litestar.contrib.jinja import JinjaTemplateEngine
+from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig
template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
-app = Litestar(plugins=[flash_plugin])
+app = Litestar(
+ plugins=[flash_plugin],
+ middleware=[ServerSideSessionConfig().middleware],
+)
diff --git a/docs/examples/plugins/flash_messages/mako.py b/docs/examples/plugins/flash_messages/mako.py
index a5ce038eab..7bc9c637ec 100644
--- a/docs/examples/plugins/flash_messages/mako.py
+++ b/docs/examples/plugins/flash_messages/mako.py
@@ -1,9 +1,13 @@
from litestar import Litestar
from litestar.contrib.mako import MakoTemplateEngine
+from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig
template_config = TemplateConfig(engine=MakoTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
-app = Litestar(plugins=[flash_plugin])
+app = Litestar(
+ plugins=[flash_plugin],
+ middleware=[ServerSideSessionConfig().middleware],
+)
diff --git a/docs/examples/plugins/flash_messages/minijinja.py b/docs/examples/plugins/flash_messages/minijinja.py
index 0ea2ce0f8e..26ae24be13 100644
--- a/docs/examples/plugins/flash_messages/minijinja.py
+++ b/docs/examples/plugins/flash_messages/minijinja.py
@@ -1,9 +1,13 @@
from litestar import Litestar
from litestar.contrib.minijinja import MiniJinjaTemplateEngine
+from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig
template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))
-app = Litestar(plugins=[flash_plugin])
+app = Litestar(
+ plugins=[flash_plugin],
+ middleware=[ServerSideSessionConfig().middleware],
+)
diff --git a/docs/examples/plugins/flash_messages/usage.py b/docs/examples/plugins/flash_messages/usage.py
index 914919ea0f..b9cfd71ab9 100644
--- a/docs/examples/plugins/flash_messages/usage.py
+++ b/docs/examples/plugins/flash_messages/usage.py
@@ -1,5 +1,6 @@
from litestar import Litestar, Request, get
from litestar.contrib.jinja import JinjaTemplateEngine
+from litestar.middleware.session.server_side import ServerSideSessionConfig
from litestar.plugins.flash import FlashConfig, FlashPlugin, flash
from litestar.response import Template
from litestar.template.config import TemplateConfig
@@ -23,4 +24,9 @@ async def index(request: Request) -> Template:
)
-app = Litestar(plugins=[flash_plugin], route_handlers=[index], template_config=template_config)
+app = Litestar(
+ plugins=[flash_plugin],
+ route_handlers=[index],
+ template_config=template_config,
+ middleware=[ServerSideSessionConfig().middleware],
+)
diff --git a/docs/release-notes/changelog.rst b/docs/release-notes/changelog.rst
index d950d092a9..211cc0b733 100644
--- a/docs/release-notes/changelog.rst
+++ b/docs/release-notes/changelog.rst
@@ -3,6 +3,318 @@
2.x Changelog
=============
+.. changelog:: 2.9.0
+ :date: 2024-06-02
+
+ .. change:: asgi lifespan msg after lifespan context exception
+ :type: bugfix
+ :pr: 3315
+
+ An exception raised within an asgi lifespan context manager would result in a "lifespan.startup.failed" message
+ being sent after we've already sent a "lifespan.startup.complete" message. This would cause uvicorn to raise a
+ ``STATE_TRANSITION_ERROR`` assertion error due to their check for that condition , if asgi lifespan is
+ forced (i.e., with ``$ uvicorn test_apps.test_app:app --lifespan on``).
+
+ E.g.,
+
+ .. code-block::
+
+ During handling of the above exception, another exception occurred:
+
+ Traceback (most recent call last):
+ File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 86, in main
+ await app(scope, self.receive, self.send)
+ File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 69, in __call__
+ return await self.app(scope, receive, send)
+ File "/home/peter/PycharmProjects/litestar/litestar/app.py", line 568, in __call__
+ await self.asgi_router.lifespan(receive=receive, send=send) # type: ignore[arg-type]
+ File "/home/peter/PycharmProjects/litestar/litestar/_asgi/asgi_router.py", line 180, in lifespan
+ await send(failure_message)
+ File "/home/peter/.local/share/pdm/venvs/litestar-dj-FOhMr-3.8/lib/python3.8/site-packages/uvicorn/lifespan/on.py", line 116, in send
+ assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
+ AssertionError: Got invalid state transition on lifespan protocol.
+
+ This PR modifies ``ASGIRouter.lifespan()`` so that it sends a shutdown failure message if we've already confirmed startup.
+
+ .. change:: bug when pydantic==1.10 is installed
+ :type: bugfix
+ :pr: 3335
+ :issue: 3334
+
+ Fix a bug introduced in #3296 where it failed to take into account that the ``pydantic_v2`` variable could be
+ ``Empty``.
+
+
+ .. change:: OpenAPI router and controller on same app.
+ :type: bugfix
+ :pr: 3338
+ :issue: 3337
+
+ Fixes an :exc`ImproperlyConfiguredException` where an app that explicitly registers an ``OpenAPIController`` on
+ the application, and implicitly uses the OpenAPI router via the `OpenAPIConfig` object. This was caused by the
+ two different handlers being given the same name as defined in ``litestar.constants``.
+
+ PR adds a distinct name for use by the handler that serves ``openapi.json`` on the controller.
+
+
+ .. change:: pydantic v2 import tests for pydantic v1.10.15
+ :type: bugfix
+ :pr: 3347
+ :issue: 3348
+
+ Fixes bug with Pydantic V1 environment test where the test was run against v2. Adds assertion for version to the test.
+
+ Fixes a bug exposed by above that relied on pydantic not having ``v1`` in the package namespace if ``v1`` is
+ installed. This doesn't hold true after pydantic's ``1.10.15`` release.
+
+
+ .. change:: schema for generic wrapped return types with DTO
+ :type: bugfix
+ :pr: 3371
+ :issue: 2929
+
+ Fix schema generated for DTOs where the supported type is wrapped in a generic outer type.
+
+
+ Prior behavior of using the ``backend.annotation`` as the basis for generating the openapi schema for the
+ represented type is not applicable for the case where the DTO supported type is wrapped in a generic outer
+ object. In that case ``backend.annotation`` only represents the type of the attribute on the generic type that
+ holds the DTO supported type annotation.
+
+ This change detects the case where we unwrap an outer generic type, and rebuilds the generic annotation in a
+ manner appropriate for schema generation, before generating the schema for the annotation. It does this by
+ substituting the DTOs transfer model for the original model in the original annotations type arguments.
+
+ .. change:: Ambiguous default warning for no signature default
+ :type: bugfix
+ :pr: 3378
+ :issue: 3372
+
+ We now only issue a single warning for the case where a default value is supplied via ``Parameter()`` and not
+ via a regular signature default.
+
+
+ .. change:: Path param consumed by dependency treated as unconsumed
+ :type: bugfix
+ :pr: 3380
+ :issue: 3369
+
+ Consider parameters defined in handler dependencies in order to determine if a path parameter has been consumed
+ for openapi generation purposes.
+
+ Fixes an issue where path parameters not consumed by the handler, but consumed by dependencies would cause an
+ :exc`ImproperlyConfiguredException`.
+
+ .. change:: "name" and "in" should not be included in openapi headers
+ :type: bugfix
+ :pr: 3417
+ :issue: 3416
+
+ Exclude the "name" and "in" fields from openapi schema generated for headers.
+
+ Add ``BaseSchemaObject._iter_fields()`` method that allows schema types to
+ define the fields that should be included in their openapi schema representation
+ and override that method for ``OpenAPIHeader``.
+
+ .. change:: top-level import of optional package
+ :type: bugfix
+ :pr: 3418
+ :issue: 3415
+
+ Fix import from ``contrib.minijinja`` without handling for case where dependency is not installed.
+
+
+ .. change:: regular handler under mounted app
+ :type: bugfix
+ :pr: 3430
+ :issue: 3429
+
+ Fix an issue where a regular handler under a mounted asgi app would prevent a
+ request from routing through the mounted application if the request path
+ contained the path of the regular handler as a substring.
+
+ .. change:: logging to file with structlog
+ :type: bugfix
+ :pr: 3425
+
+ Fix and issue with converting ``StructLoggingConfig`` to dict during call to
+ ``configure()`` when the config object has a custom logger factory that
+ references a ``TextIO`` object, which cannot be pickled.
+
+ .. change:: clear session cookie if new session exceeds ``CHUNK_SIZE``
+ :type: bugfix
+ :pr: 3446
+ :issue: 3441
+
+ Fix an issue where the connection session cookie is not cleared if the response
+ session is stored across multiple cookies.
+
+ .. change:: flash messages were not displayed on Redirect
+ :type: bugfix
+ :pr: 3420
+ :issue: 3325
+
+ Fix an issue where flashed messages were not shown after a redirect
+
+ .. change:: Validation of optional sequence in multipart data with one value
+ :type: bugfix
+ :pr: 3408
+ :issue: 3407
+
+ A ``Sequence[UploadFile] | None`` would not pass validation when a single value
+ was provided for a structured type, e.g. dataclass.
+
+ .. change:: field not optional if default value
+ :type: bugfix
+ :pr: 3476
+ :issue: 3471
+
+ Fix issue where a pydantic v1 field annotation is wrapped with ``Optional`` if
+ it is marked not required, but has a default value.
+
+ .. change:: prevent starting multiple responses
+ :type: bugfix
+ :pr: 3479
+
+ Prevent the app's exception handler middleware from starting a response after
+ one has already started.
+
+ When something in the middleware stack raises an exception after a
+ "http.response.start" message has already been sent, we end up with long
+ exception chains that obfuscate the original exception.
+
+ This change implements tracking of when a response has started, and if so, we
+ immediately raise the exception instead of sending it through the usual exception
+ handling code path.
+
+ .. change:: logging middleware with multi-body response
+ :type: bugfix
+ :pr: 3478
+ :issue: 3477
+
+ Prevent logging middleware from failing with a :exc:`KeyError` when a response
+ sends multiple "http.response.body" messages.
+
+ .. change:: handle dto type nested in mapping
+ :type: bugfix
+ :pr: 3486
+ :issue: 3463
+
+ Added handling for transferring data from a transfer model, to a DTO supported
+ instance when the DTO supported type is nested in a mapping.
+
+ I.e, handles this case:
+
+ .. code-block:: python
+
+ @dataclass
+ class NestedDC:
+ a: int
+ b: str
+
+ @dataclass
+ class DC:
+ nested_mapping: Dict[str, NestedDC]
+
+ .. change:: examples omitted in schema produced by dto
+ :type: bugfix
+ :pr: 3510
+ :issue: 3505
+
+ Fixes issue where a ``BodyKwarg`` instance provided as metadata to a data type
+ annotation was ignored for OpenAPI schema generation when the data type is
+ managed by a DTO.
+
+ .. change:: fix handling validation of subscribed generics
+ :type: bugfix
+ :pr: 3519
+
+ Fix a bug that would lead to a :exc:`TypeError` when subscribed generics were
+ used in a route handler signature and subject to validation.
+
+ .. code-block:: python
+
+ from typing import Generic, TypeVar
+ from litestar import get
+ from litestar.testing import create_test_client
+
+ T = TypeVar("T")
+
+ class Foo(Generic[T]):
+ pass
+
+ async def provide_foo() -> Foo[str]:
+ return Foo()
+
+ @get("/", dependencies={"foo": provide_foo})
+ async def something(foo: Foo[str]) -> None:
+ return None
+
+ with create_test_client([something]) as client:
+ client.get("/")
+
+
+ .. change:: exclude static file from schema
+ :type: bugfix
+ :pr: 3509
+ :issue: 3374
+
+ Exclude static file routes created with ``create_static_files_router`` from the OpenAPI schema by default
+
+ .. change:: use re.match instead of re.search for mounted app path (#3501)
+ :type: bugfix
+ :pr: 3511
+ :issue: 3501
+
+ When mounting an app, path resolution uses ``re.search`` instead or ``re.match``,
+ thus mounted app matches any path which contains mount path.
+
+ .. change:: do not log exceptions twice, deprecate ``traceback_line_limit`` and fix ``pretty_print_tty``
+ :type: bugfix
+ :pr: 3507
+ :issue: 3228
+
+ * The wording of the log message, when logging an exception, has been updated.
+ * For structlog, the ``traceback`` field in the log message (which contained a
+ truncated stacktrace) has been removed. The ``exception`` field is still around and contains the full stacktrace.
+ * The option ``traceback_line_limit`` has been deprecated. The value is now ignored, the full stacktrace will be logged.
+
+
+ .. change:: YAML schema dump
+ :type: bugfix
+ :pr: 3537
+
+ Fix an issue in the OpenAPI YAML schema dump logic of ``OpenAPIController``
+ where the endpoint for the OpenAPI YAML schema file returns an empty response
+ if a request has been made to the OpenAPI JSON schema previously due to an
+ incorrect variable check.
+
+
+ .. change:: Add async ``websocket_connect`` to ``AsyncTestClient``
+ :type: feature
+ :pr: 3328
+ :issue: 3133
+
+ Add async ``websocket_connect`` to ``AsyncTestClient``
+
+
+ .. change:: add ``SecretString`` and ``SecretBytes`` datastructures
+ :type: feature
+ :pr: 3322
+ :issue: 1312, 3248
+
+
+ Implement ``SecretString`` and ``SecretBytes`` data structures to hide sensitive
+ data in tracebacks, etc.
+
+ .. change:: Deprecate subclassing route handler decorators
+ :type: feature
+ :pr: 3439
+
+ Deprecation for the 2.x release line of the semantic route handler classes
+ removed in #3436.
+
+
.. changelog:: 2.8.3
:date: 2024-05-06
diff --git a/docs/usage/caching.rst b/docs/usage/caching.rst
index d3c26bdc12..3eaf68cb8e 100644
--- a/docs/usage/caching.rst
+++ b/docs/usage/caching.rst
@@ -7,13 +7,9 @@ Caching responses
Sometimes it's desirable to cache some responses, especially if these involve expensive calculations, or when polling is
expected. Litestar comes with a simple mechanism for caching:
-.. code-block:: python
-
- from litestar import get
-
-
- @get("/cached-path", cache=True)
- def my_cached_handler() -> str: ...
+.. literalinclude:: /examples/caching/cache.py
+ :language: python
+ :lines: 1, 4-8
By setting :paramref:`~litestar.handlers.HTTPRouteHandler.cache` to ``True``, the response from the handler
will be cached. If no ``cache_key_builder`` is set in the route handler, caching for the route handler will be
@@ -25,31 +21,22 @@ enabled for the :attr:`~.config.response_cache.ResponseCacheConfig.default_expir
Alternatively you can specify the number of seconds to cache the responses from the given handler like so:
-.. code-block:: python
+.. literalinclude:: /examples/caching/cache.py
+ :language: python
:caption: Caching the response for 120 seconds by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache`
parameter to the number of seconds to cache the response.
+ :lines: 1, 9-13
:emphasize-lines: 4
- from litestar import get
-
-
- @get("/cached-path", cache=120) # seconds
- def my_cached_handler() -> str: ...
-
-
If you want the response to be cached indefinitely, you can pass the :class:`~.config.response_cache.CACHE_FOREVER`
sentinel instead:
-.. code-block:: python
+.. literalinclude:: /examples/caching/cache.py
+ :language: python
:caption: Caching the response indefinitely by setting the :paramref:`~litestar.handlers.HTTPRouteHandler.cache`
parameter to :class:`~litestar.config.response_cache.CACHE_FOREVER`.
-
- from litestar import get
- from litestar.config.response_cache import CACHE_FOREVER
-
-
- @get("/cached-path", cache=CACHE_FOREVER)
- def my_cached_handler() -> str: ...
+ :lines: 1, 3, 14-18
+ :emphasize-lines: 5
Configuration
-------------
@@ -63,45 +50,20 @@ Changing where data is stored
By default, caching will use the :class:`~.stores.memory.MemoryStore`, but it can be configured with
any :class:`~.stores.base.Store`, for example :class:`~.stores.redis.RedisStore`:
-.. code-block:: python
+.. literalinclude:: /examples/caching/redis_store.py
+ :language: python
:caption: Using Redis as the cache store.
- from litestar.config.cache import ResponseCacheConfig
- from litestar.stores.redis import RedisStore
-
- redis_store = RedisStore(url="redis://localhost/", port=6379, db=0)
-
- cache_config = ResponseCacheConfig(store=redis_store)
-
-
Specifying a cache key builder
++++++++++++++++++++++++++++++
Litestar uses the request's path + sorted query parameters as the cache key. This can be adjusted by providing a
"key builder" function, either at application or route handler level.
-.. code-block:: python
+.. literalinclude:: /examples/caching/key_builder.py
+ :language: python
:caption: Using a custom cache key builder.
- from litestar import Litestar, Request
- from litestar.config.cache import ResponseCacheConfig
-
-
- def key_builder(request: Request) -> str:
- return request.url.path + request.headers.get("my-header", "")
-
-
- app = Litestar([], cache_config=ResponseCacheConfig(key_builder=key_builder))
-
-.. code-block:: python
+.. literalinclude:: /examples/caching/key_builder_for_route_handler.py
+ :language: python
:caption: Using a custom cache key builder for a specific route handler.
-
- from litestar import Litestar, Request, get
-
-
- def key_builder(request: Request) -> str:
- return request.url.path + request.headers.get("my-header", "")
-
-
- @get("/cached-path", cache=True, cache_key_builder=key_builder)
- def cached_handler() -> str: ...
diff --git a/docs/usage/logging.rst b/docs/usage/logging.rst
index c39861aea4..85269282c6 100644
--- a/docs/usage/logging.rst
+++ b/docs/usage/logging.rst
@@ -18,7 +18,7 @@ Application and request level loggers can be configured using the :class:`~lites
logging_config = LoggingConfig(
- root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]},
+ root={"level": "INFO", "handlers": ["queue_listener"]},
formatters={
"standard": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}
},
diff --git a/docs/usage/plugins/flash_messages.rst b/docs/usage/plugins/flash_messages.rst
index 8ff46b8db6..40e61554bc 100644
--- a/docs/usage/plugins/flash_messages.rst
+++ b/docs/usage/plugins/flash_messages.rst
@@ -57,6 +57,7 @@ Breakdown
+++++++++
#. Here we import the requires classes and functions from the Litestar package and related plugins.
+#. Flash messages requires a valid session configuration, so we create and enable the ``ServerSideSession`` middleware.
#. We then create our ``TemplateConfig`` and ``FlashConfig`` instances, each setting up the configuration for
the template engine and flash messages, respectively.
#. A single route handler named ``index`` is defined using the ``@get()`` decorator.
diff --git a/litestar/config/csrf.py b/litestar/config/csrf.py
index 5094a5b7c8..225dc23daa 100644
--- a/litestar/config/csrf.py
+++ b/litestar/config/csrf.py
@@ -34,7 +34,7 @@ class CSRFConfig:
"""The value to set in the ``SameSite`` attribute of the cookie."""
cookie_domain: str | None = field(default=None)
"""Specifies which hosts can receive the cookie."""
- safe_methods: set[Method] = field(default_factory=lambda: {"GET", "HEAD"})
+ safe_methods: set[Method] = field(default_factory=lambda: {"GET", "HEAD", "OPTIONS"})
"""A set of "safe methods" that can set the cookie."""
exclude: str | list[str] | None = field(default=None)
"""A pattern or list of patterns to skip in the CSRF middleware."""
diff --git a/litestar/exceptions/responses/__init__.py b/litestar/exceptions/responses/__init__.py
index e999e13a4f..9ed1df1c52 100644
--- a/litestar/exceptions/responses/__init__.py
+++ b/litestar/exceptions/responses/__init__.py
@@ -37,7 +37,6 @@ def to_response(self, request: Request | None = None) -> Response:
Returns:
A response instance.
"""
- from litestar.response import Response
content: Any = {k: v for k, v in asdict(self).items() if k not in ("headers", "media_type") and v is not None}
type_encoders = _debug_response._get_type_encoders_for_request(request) if request is not None else None
diff --git a/litestar/logging/config.py b/litestar/logging/config.py
index c084c5d034..2b1587c89f 100644
--- a/litestar/logging/config.py
+++ b/litestar/logging/config.py
@@ -10,7 +10,7 @@
from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException
from litestar.serialization.msgspec_hooks import _msgspec_json_encoder
from litestar.utils.dataclass import simple_asdict
-from litestar.utils.deprecation import deprecated
+from litestar.utils.deprecation import deprecated, warn_deprecation
__all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig")
@@ -90,34 +90,45 @@ def _get_default_handlers() -> dict[str, dict[str, Any]]:
def _default_exception_logging_handler_factory(
- is_struct_logger: bool, traceback_line_limit: int
+ is_struct_logger: bool,
+ traceback_line_limit: int,
) -> ExceptionLoggingHandler:
"""Create an exception logging handler function.
Args:
is_struct_logger: Whether the logger is a structlog instance.
traceback_line_limit: Maximal number of lines to log from the
- traceback.
+ traceback. This parameter is deprecated and ignored.
Returns:
An exception logging handler.
"""
- def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
- # we limit the length of the stack trace to 20 lines.
- first_line = tb.pop(0)
+ if traceback_line_limit != -1:
+ warn_deprecation(
+ version="2.9.0",
+ deprecated_name="traceback_line_limit",
+ kind="parameter",
+ info="The value is ignored. Use a custom 'exception_logging_handler' instead.",
+ removal_in="3.0",
+ )
+
+ if is_struct_logger:
- if is_struct_logger:
+ def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
logger.exception(
- "Uncaught Exception",
+ "Uncaught exception",
connection_type=scope["type"],
path=scope["path"],
- traceback="".join(tb[-traceback_line_limit:]),
)
- else:
- stack_trace = first_line + "".join(tb[-traceback_line_limit:])
+
+ else:
+
+ def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
logger.exception(
- "exception raised on %s connection to route %s\n\n%s", scope["type"], scope["path"], stack_trace
+ "Uncaught exception (connection_type=%s, path=%s):",
+ scope["type"],
+ scope["path"],
)
return _default_exception_logging_handler
@@ -131,7 +142,11 @@ class BaseLoggingConfig(ABC):
log_exceptions: Literal["always", "debug", "never"]
"""Should exceptions be logged, defaults to log exceptions when ``app.debug == True``'"""
traceback_line_limit: int
- """Max number of lines to print for exception traceback"""
+ """Max number of lines to print for exception traceback.
+
+ .. deprecated:: 2.9.0
+ This parameter is deprecated and ignored. It will be removed in a future release.
+ """
exception_logging_handler: ExceptionLoggingHandler | None
"""Handler function for logging exceptions."""
@@ -205,8 +220,12 @@ class LoggingConfig(BaseLoggingConfig):
"""Should the root logger be configured, defaults to True for ease of configuration."""
log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
- traceback_line_limit: int = field(default=20)
- """Max number of lines to print for exception traceback"""
+ traceback_line_limit: int = field(default=-1)
+ """Max number of lines to print for exception traceback.
+
+ .. deprecated:: 2.9.0
+ This parameter is deprecated and ignored. It will be removed in a future release.
+ """
exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
"""Handler function for logging exceptions."""
@@ -421,8 +440,12 @@ class StructLoggingConfig(BaseLoggingConfig):
"""Whether to cache the logger configuration and reuse."""
log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
"""Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
- traceback_line_limit: int = field(default=20)
- """Max number of lines to print for exception traceback"""
+ traceback_line_limit: int = field(default=-1)
+ """Max number of lines to print for exception traceback.
+
+ .. deprecated:: 2.9.0
+ This parameter is deprecated and ignored. It will be removed in a future release.
+ """
exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
"""Handler function for logging exceptions."""
pretty_print_tty: bool = field(default=True)
@@ -430,9 +453,9 @@ class StructLoggingConfig(BaseLoggingConfig):
def __post_init__(self) -> None:
if self.processors is None:
- self.processors = default_structlog_processors(not sys.stderr.isatty() and self.pretty_print_tty)
+ self.processors = default_structlog_processors(as_json=self.as_json())
if self.logger_factory is None:
- self.logger_factory = default_logger_factory(not sys.stderr.isatty() and self.pretty_print_tty)
+ self.logger_factory = default_logger_factory(as_json=self.as_json())
if self.log_exceptions != "never" and self.exception_logging_handler is None:
self.exception_logging_handler = _default_exception_logging_handler_factory(
is_struct_logger=True, traceback_line_limit=self.traceback_line_limit
@@ -445,15 +468,16 @@ def __post_init__(self) -> None:
formatters={
"standard": {
"()": structlog.stdlib.ProcessorFormatter,
- "processors": default_structlog_standard_lib_processors(
- as_json=not sys.stderr.isatty() and self.pretty_print_tty
- ),
+ "processors": default_structlog_standard_lib_processors(as_json=self.as_json()),
}
}
)
except ImportError:
self.standard_lib_logging_config = LoggingConfig()
+ def as_json(self) -> bool:
+ return not (sys.stderr.isatty() and self.pretty_print_tty)
+
def configure(self) -> GetLogger:
"""Return logger with the given configuration.
diff --git a/litestar/openapi/controller.py b/litestar/openapi/controller.py
index effc2ea3d3..61f1148b1d 100644
--- a/litestar/openapi/controller.py
+++ b/litestar/openapi/controller.py
@@ -172,7 +172,7 @@ def retrieve_schema_yaml(self, request: Request[Any, Any, Any]) -> ASGIResponse:
from yaml import dump as dump_yaml
if self.should_serve_endpoint(request):
- if not self._dumped_json_schema:
+ if not self._dumped_yaml_schema:
schema_json = decode_json(self._get_schema_as_json(request))
schema_yaml = dump_yaml(schema_json, default_flow_style=False)
self._dumped_yaml_schema = schema_yaml.encode("utf-8")
diff --git a/litestar/plugins/flash.py b/litestar/plugins/flash.py
index ebede72ed8..b7d67f7eb1 100644
--- a/litestar/plugins/flash.py
+++ b/litestar/plugins/flash.py
@@ -20,6 +20,7 @@
from collections.abc import Callable
from litestar.config.app import AppConfig
+ from litestar.connection.base import AuthT, StateT, UserT
from litestar.template import TemplateConfig
@@ -69,7 +70,7 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig:
def flash(
- request: Request,
+ request: Request[UserT, AuthT, StateT],
message: Any,
category: str,
) -> None:
diff --git a/pyproject.toml b/pyproject.toml
index b811d0054c..a083d64bdb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,7 +61,7 @@ maintainers = [
name = "litestar"
readme = "README.md"
requires-python = ">=3.8,<4.0"
-version = "2.8.3"
+version = "2.9.0"
[project.urls]
Blog = "https://blog.litestar.dev"
diff --git a/tests/unit/test_logging/test_logging_config.py b/tests/unit/test_logging/test_logging_config.py
index e318ed730f..80731ffe38 100644
--- a/tests/unit/test_logging/test_logging_config.py
+++ b/tests/unit/test_logging/test_logging_config.py
@@ -219,3 +219,16 @@ def test_customizing_handler(handlers: Any, expected_handler_class: Any, monkeyp
else:
formatter = root_logger_handler.formatter
assert formatter._fmt == log_format
+
+
+@pytest.mark.parametrize(
+ "traceback_line_limit, expected_warning_deprecation_called",
+ [
+ [-1, False],
+ [20, True],
+ ],
+)
+def test_traceback_line_limit_deprecation(traceback_line_limit: int, expected_warning_deprecation_called: bool) -> None:
+ with patch("litestar.logging.config.warn_deprecation") as mock_warning_deprecation:
+ LoggingConfig(traceback_line_limit=traceback_line_limit)
+ assert mock_warning_deprecation.called is expected_warning_deprecation_called
diff --git a/tests/unit/test_logging/test_structlog_config.py b/tests/unit/test_logging/test_structlog_config.py
index 4f8e695496..30e80b7e5c 100644
--- a/tests/unit/test_logging/test_structlog_config.py
+++ b/tests/unit/test_logging/test_structlog_config.py
@@ -1,6 +1,7 @@
import datetime
import sys
from typing import Callable
+from unittest.mock import patch
import pytest
import structlog
@@ -155,3 +156,19 @@ def test_structlog_config_specify_processors(capsys: CaptureFixture) -> None:
{"key": "value1", "event": "message1"},
{"key": "value2", "event": "message2"},
]
+
+
+@pytest.mark.parametrize(
+ "isatty, pretty_print_tty, expected_as_json",
+ [
+ (True, True, False),
+ (True, False, True),
+ (False, True, True),
+ (False, False, True),
+ ],
+)
+def test_structlog_config_as_json(isatty: bool, pretty_print_tty: bool, expected_as_json: bool) -> None:
+ with patch("litestar.logging.config.sys.stderr.isatty") as isatty_mock:
+ isatty_mock.return_value = isatty
+ logging_config = StructLoggingConfig(pretty_print_tty=pretty_print_tty)
+ assert logging_config.as_json() is expected_as_json
diff --git a/tests/unit/test_middleware/test_exception_handler_middleware.py b/tests/unit/test_middleware/test_exception_handler_middleware.py
index cfa10fd7e8..a1540d353c 100644
--- a/tests/unit/test_middleware/test_exception_handler_middleware.py
+++ b/tests/unit/test_middleware/test_exception_handler_middleware.py
@@ -3,7 +3,6 @@
from unittest.mock import MagicMock
import pytest
-from _pytest.capture import CaptureFixture
from pytest_mock import MockerFixture
from starlette.exceptions import HTTPException as StarletteHTTPException
from structlog.testing import capture_logs
@@ -205,12 +204,10 @@ def handler() -> None:
if should_log:
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"
- )
+ assert caplog.records[0].message.startswith("Uncaught exception (connection_type=http, path=/test):")
else:
assert not caplog.records
- assert "exception raised on http connection request to route /test" not in response.text
+ assert "Uncaught exception" not in response.text
@pytest.mark.parametrize(
@@ -228,7 +225,6 @@ def handler() -> None:
)
def test_exception_handler_struct_logging(
get_logger: "GetLogger",
- capsys: CaptureFixture,
is_debug: bool,
logging_config: Optional[LoggingConfig],
should_log: bool,
@@ -251,50 +247,12 @@ def handler() -> None:
assert len(cap_logs) == 1
assert cap_logs[0].get("connection_type") == "http"
assert cap_logs[0].get("path") == "/test"
- assert cap_logs[0].get("traceback")
- assert cap_logs[0].get("event") == "Uncaught Exception"
+ assert cap_logs[0].get("event") == "Uncaught exception"
assert cap_logs[0].get("log_level") == "error"
else:
assert not cap_logs
-def test_traceback_truncate_default_logging(
- get_logger: "GetLogger",
- caplog: "LogCaptureFixture",
-) -> None:
- @get("/test")
- def handler() -> None:
- raise ValueError("Test debug exception")
-
- app = Litestar([handler], logging_config=LoggingConfig(log_exceptions="always", traceback_line_limit=1))
-
- 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 "Internal Server Error" in response.text
-
- assert len(caplog.records) == 1
- assert caplog.records[0].levelname == "ERROR"
- assert caplog.records[0].message == (
- "exception raised on http connection to route /test\n\nTraceback (most recent call last):\nValueError: Test debug exception\n"
- )
-
-
-def test_traceback_truncate_struct_logging() -> None:
- @get("/test")
- def handler() -> None:
- raise ValueError("Test debug exception")
-
- app = Litestar([handler], logging_config=StructLoggingConfig(log_exceptions="always", traceback_line_limit=1))
-
- with TestClient(app=app) as client, capture_logs() as cap_logs:
- response = client.get("/test")
- assert response.status_code == HTTP_500_INTERNAL_SERVER_ERROR
- assert len(cap_logs) == 1
- assert cap_logs[0].get("traceback") == "ValueError: Test debug exception\n"
-
-
def handler(_: Any, __: Any) -> Any:
return None
@@ -358,9 +316,7 @@ def handler() -> None:
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"
- )
+ assert caplog.records[0].message.startswith("Uncaught exception (connection_type=http, path=/test):")
def test_get_symbol_name_where_type_doesnt_support_bool() -> None:
diff --git a/tests/unit/test_openapi/test_integration.py b/tests/unit/test_openapi/test_integration.py
index 2a84b32fdf..31af60e5fa 100644
--- a/tests/unit/test_openapi/test_integration.py
+++ b/tests/unit/test_openapi/test_integration.py
@@ -112,6 +112,23 @@ def test_openapi_json_not_allowed(person_controller: type[Controller], pet_contr
assert response.status_code == HTTP_404_NOT_FOUND
+@pytest.mark.parametrize(
+ "schema_paths",
+ [
+ ("/schema/openapi.json", "/schema/openapi.yaml"),
+ ("/schema/openapi.yaml", "/schema/openapi.json"),
+ ],
+)
+def test_openapi_controller_internal_schema_conversion(schema_paths: list[str]) -> None:
+ openapi_config = OpenAPIConfig("Example API", "1.0.0", openapi_controller=OpenAPIController)
+
+ with create_test_client([], openapi_config=openapi_config) as client:
+ for schema_path in schema_paths:
+ response = client.get(schema_path)
+ assert response.status_code == HTTP_200_OK
+ assert "Example API" in response.text
+
+
def test_openapi_custom_path(openapi_controller: type[OpenAPIController] | None) -> None:
openapi_config = OpenAPIConfig(
title="my title", version="1.0.0", path="/custom_schema_path", openapi_controller=openapi_controller