Skip to content

Commit 1309144

Browse files
authored
chore: Release 2.17 (#4253)
1 parent 03b5813 commit 1309144

File tree

10 files changed

+7153
-3084
lines changed

10 files changed

+7153
-3084
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ jobs:
5656
- name: Install uv
5757
uses: astral-sh/setup-uv@v6
5858
with:
59-
version: "0.6.12"
59+
version: "0.8.8"
6060
enable-cache: true
6161

6262
- name: Install dependencies

docs/_static/favicon.png

7.5 KB
Loading

docs/_static/logo.svg

Lines changed: 31 additions & 0 deletions
Loading

docs/release-notes/changelog.rst

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,83 @@
11
:orphan:
22

3-
2.x Changelog
4-
=============
3+
Litestar 2 Changelog
4+
====================
5+
6+
.. changelog:: 2.17.0
7+
:date: 2025-08-09
8+
9+
.. change:: Fix CRLF injection vulnerability in exception logging
10+
:type: bugfix
11+
12+
Fix a CRLF vulnerability in the exception logging where Litestar included the
13+
raw request path in the logged exception, allowing potential attackers to inject
14+
newlines into the log message.
15+
16+
.. change:: OpenAPI: Fix empty response body when using DTO and ``Response[T]`` annotation
17+
:type: bugfix
18+
:pr: 4158
19+
:issue: 3888
20+
21+
Fix a bug that result in an empty response body in the OpenAPI schema when a
22+
``return_dto`` was defined on a handler with a generic ``Response`` annotation,
23+
such as
24+
25+
.. code-block:: python
26+
27+
@get("/get_items", return_dto=ItemReadDTO)
28+
async def get_items() -> Response[list[Item]]:
29+
return Response(
30+
content=[
31+
Item(id=1, name="Item 1", secret="123"),
32+
],
33+
)
34+
35+
36+
.. change:: OpenAPI: Ensure deterministic order of schema types
37+
:type: bugfix
38+
:pr: 4239
39+
:issue: 3646
40+
41+
Fix a bug that would result in a non-deterministic ordering of ``Literal`` /
42+
Enum type unions, such as ``Literal[*] | None``
43+
44+
.. change:: Ensure dependency cleanup of ``yield`` dependency happens in reverse order
45+
:type: bugfix
46+
:pr: 4246
47+
48+
Fix a regression in the DI system that would cause generator dependencies to
49+
be cleaned up in the order they were entered, instead of the reverse order.
50+
51+
.. change:: OpenAPI: Add option to exclude parameter from schema
52+
:type: feature
53+
:pr: 4177
54+
55+
Add a new ``exclude_from_schema`` parameter to
56+
:func:`~litestar.params.Parameter` that allows to exclude a specific parameter
57+
from the OpenAPI schema.
58+
59+
.. change:: OpenAPI: Extend support for Pydantic's custom date(time) types
60+
:type: feature
61+
:pr: 4218
62+
:issue: 4217
63+
64+
Add full OpenAPI schema support for Pydantic's custom date(time) types:
65+
66+
- ``PastDate``
67+
- ``FutureDate``
68+
- ``PastDatetime``
69+
- ``FutureDatetime``
70+
- ``AwareDatetime``
71+
- ``NaiveDatetime``
72+
73+
.. change:: Make ``ReceiveRoutePlugin`` public
74+
:type: feature
75+
:pr: 4220
76+
77+
Make the previously internally used
78+
:class:`litestar.plugins.ReceiveRoutePlugin` public.
79+
80+
581
.. changelog:: 2.16.0
682
:date: 2025-05-04
783

litestar/_openapi/schema_generation/examples.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def _normalize_example_value(value: Any) -> Any:
4141
# UnsetType not part of the Union
4242
pass
4343

44-
value = unwrap_annotation(annotation=value, random=ExampleFactory.__random__)
44+
value = unwrap_annotation(annotation=value)
4545
if isinstance(value, (Decimal, float)):
4646
value = round(float(value), 2)
4747
if isinstance(value, Enum):
@@ -63,7 +63,6 @@ def _create_field_meta(field: FieldDefinition) -> FieldMeta:
6363
annotation=field.annotation,
6464
default=field.default if field.default is not Empty else Null,
6565
name=field.name,
66-
random=ExampleFactory.__random__,
6766
)
6867

6968

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ maintainers = [
6464
name = "litestar"
6565
readme = "docs/PYPI_README.md"
6666
requires-python = ">=3.8,<4.0"
67-
version = "2.16.0"
67+
version = "2.17.0"
6868

6969
[project.urls]
7070
Blog = "https://blog.litestar.dev"
@@ -103,7 +103,7 @@ pydantic = [
103103
"pydantic-extra-types!=2.9.0; python_version < \"3.9\"",
104104
"pydantic-extra-types; python_version >= \"3.9\"",
105105
]
106-
redis = ["redis[hiredis]>=4.4.4"]
106+
redis = ["redis[hiredis]>=4.4.4,<5.3"] # 5.3.0 introduced some issues with the channels plugin; need to investigate
107107
sqlalchemy = ["advanced-alchemy>=0.2.2"]
108108
standard = [
109109
"jinja2",
@@ -140,8 +140,7 @@ dev = [
140140
"aiosqlite",
141141
"asyncpg>=0.29.0",
142142
"psycopg[pool,binary]>=3.1.10,<3.2; python_version < \"3.13\"",
143-
"psycopg[pool,c]; python_version >= \"3.13\" and sys_platform == 'linux'",
144-
"psycopg[pool]; python_version >= \"3.13\" and sys_platform != 'linux'",
143+
"psycopg[pool]<3.2.4; python_version >= \"3.13\"", # Bug in 3.2.4: https://github.com/psycopg/psycopg/issues/1128
145144
"psycopg2-binary",
146145
"psutil>=5.9.8",
147146
"hypercorn>=0.16.0",
@@ -177,7 +176,7 @@ linting = [
177176
]
178177
test = [
179178
"covdefaults",
180-
"pytest",
179+
"pytest==8.3.4", # 8.4.1 causes some issues with asyncio for us; need to investigate
181180
"pytest-asyncio<=0.24.0; python_version < \"3.9\"",
182181
"pytest-asyncio>0.24.0; python_version >= \"3.9\"",
183182
"pytest-cov",
@@ -226,6 +225,7 @@ filterwarnings = [
226225
"ignore: Dropping max_length:litestar.exceptions.LitestarWarning:litestar.contrib.piccolo",
227226
"ignore: Python Debugger on exception enabled:litestar.exceptions.LitestarWarning:",
228227
"ignore: datetime.datetime.utcnow:DeprecationWarning:time_machine",
228+
"ignore:Use of deprecated default '__check_model__':DeprecationWarning:polyfactory:",
229229
]
230230
markers = [
231231
"sqlalchemy_integration: SQLAlchemy integration tests",

tests/e2e/test_logging/test_structlog_to_file.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,28 @@ def structlog_reset() -> Iterator[None]:
2929
def test_structlog_to_file(tmp_path: Path) -> None:
3030
log_file = tmp_path / "log.log"
3131

32-
logging_config = StructlogConfig(
33-
structlog_logging_config=StructLoggingConfig(
34-
logger_factory=structlog.WriteLoggerFactory(file=log_file.open("wt")),
35-
processors=default_structlog_processors(
36-
json_serializer=lambda v, **_: str(default_json_serializer(v), "utf-8")
32+
with log_file.open("wt") as file_handle:
33+
logging_config = StructlogConfig(
34+
structlog_logging_config=StructLoggingConfig(
35+
logger_factory=structlog.WriteLoggerFactory(file=file_handle),
36+
processors=default_structlog_processors(
37+
json_serializer=lambda v, **_: str(default_json_serializer(v), "utf-8")
38+
),
3739
),
38-
),
39-
)
40+
)
4041

41-
logger = structlog.get_logger()
42+
logger = structlog.get_logger()
4243

43-
@get("/")
44-
def handler() -> str:
45-
logger.info("handled", hello="world")
46-
return "hello"
44+
@get("/")
45+
def handler() -> str:
46+
logger.info("handled", hello="world")
47+
return "hello"
4748

48-
app = Litestar(route_handlers=[handler], plugins=[StructlogPlugin(config=logging_config)], debug=True)
49+
app = Litestar(route_handlers=[handler], plugins=[StructlogPlugin(config=logging_config)], debug=True)
4950

50-
with TestClient(app) as client:
51-
resp = client.get("/")
52-
assert resp.text == "hello"
51+
with TestClient(app) as client:
52+
resp = client.get("/")
53+
assert resp.text == "hello"
5354

5455
logged_data = [json.loads(line) for line in log_file.read_text().splitlines()]
5556
assert logged_data == [

tests/unit/test_cli/test_cli.py

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
1-
import importlib
2-
import sys
31
from typing import TYPE_CHECKING
4-
from unittest.mock import MagicMock
52

63
import pytest
74

8-
from tests.unit.test_cli import CREATE_APP_FILE_CONTENT
9-
from tests.unit.test_cli.conftest import CreateAppFileFixture
10-
11-
try:
12-
from rich_click import group
13-
except ImportError:
14-
from click import group # type:ignore[no-redef]
15-
16-
import litestar.cli._utils
17-
import litestar.cli.main
18-
from litestar import Litestar
195
from litestar.cli._utils import _format_is_enabled
206
from litestar.cli.main import litestar_group as cli_command
7+
from tests.unit.test_cli import CREATE_APP_FILE_CONTENT
8+
from tests.unit.test_cli.conftest import CreateAppFileFixture
219

2210
if TYPE_CHECKING:
2311
from pathlib import Path
@@ -64,34 +52,6 @@ def test_info_command_with_app_dir(
6452
mock.assert_called_once()
6553

6654

67-
@pytest.mark.xdist_group("cli_autodiscovery")
68-
def test_register_commands_from_entrypoint(mocker: "MockerFixture", runner: "CliRunner", app_file: "Path") -> None:
69-
mock_command_callback = MagicMock()
70-
71-
@group()
72-
def custom_group() -> None:
73-
pass
74-
75-
@custom_group.command()
76-
def custom_command(app: Litestar) -> None:
77-
mock_command_callback()
78-
79-
mock_entry_point = MagicMock()
80-
mock_entry_point.load = lambda: custom_group
81-
if sys.version_info < (3, 10):
82-
mocker.patch("importlib_metadata.entry_points", return_value=[mock_entry_point])
83-
else:
84-
mocker.patch("importlib.metadata.entry_points", return_value=[mock_entry_point])
85-
86-
importlib.reload(litestar.cli._utils)
87-
cli_command = importlib.reload(litestar.cli.main).litestar_group
88-
89-
result = runner.invoke(cli_command, f"--app={app_file.stem}:app custom-group custom-command")
90-
91-
assert result.exit_code == 0
92-
mock_command_callback.assert_called_once()
93-
94-
9555
@pytest.mark.xdist_group("cli_autodiscovery")
9656
@pytest.mark.parametrize("invalid_app", ["invalid", "info_with_app_dir"])
9757
def test_incorrect_app_argument(

tests/unit/test_connection/test_request.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,11 +240,8 @@ async def handler(request: Request) -> bytes:
240240
response = client.post("/")
241241
assert response.json() == {"body": ""}
242242

243-
response = client.post("/", json={"a": "123"})
244-
assert response.json() == {"body": '{"a":"123"}'}
245-
246-
response = client.post("/", content="abc")
247-
assert response.json() == {"body": "abc"}
243+
response = client.post("/", content="foo")
244+
assert response.json() == {"body": "foo"}
248245

249246

250247
def test_request_stream() -> None:
@@ -259,9 +256,6 @@ async def handler(request: Request) -> bytes:
259256
response = client.post("/")
260257
assert response.json() == {"body": ""}
261258

262-
response = client.post("/", json={"a": "123"})
263-
assert response.json() == {"body": '{"a":"123"}'}
264-
265259
response = client.post("/", content="abc")
266260
assert response.json() == {"body": "abc"}
267261

0 commit comments

Comments
 (0)