Skip to content

Commit ecc67be

Browse files
authored
fix(litestar): preserve app_config.plugins identity in on_app_init (#441)
## Summary - `SQLSpecPlugin.on_app_init` rebound `app_config.plugins` to a fresh list, breaking Litestar's mid-iteration plugin discovery — Litestar captures the list reference once via a genex (`litestar/app.py:399`), so any plugin running after SQLSpec that calls `app_config.plugins.append(...)` was silently dropped. - Surfaced with `litestar-vite`'s `VitePlugin` -> `InertiaPlugin`: registering `SQLSpecPlugin` before `VitePlugin` caused Inertia's handler-wrapping to never install. Reversing the order masked the bug. - Fix: mutate `app_config.plugins` in place via `.append`, and drop the unused `or []` (`AppConfig.plugins` is `field(default_factory=list)`, never `None`).
1 parent aa7a472 commit ecc67be

2 files changed

Lines changed: 102 additions & 4 deletions

File tree

sqlspec/extensions/litestar/plugin.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -481,10 +481,8 @@ def store_sqlspec_in_state() -> None:
481481
if signature_namespace:
482482
app_config.signature_namespace.update(signature_namespace)
483483

484-
existing_plugins = list(app_config.plugins or [])
485-
if not any(isinstance(p, _OffsetPaginationSchemaPlugin) for p in existing_plugins):
486-
existing_plugins.append(_OffsetPaginationSchemaPlugin())
487-
app_config.plugins = existing_plugins
484+
if not any(isinstance(p, _OffsetPaginationSchemaPlugin) for p in app_config.plugins):
485+
app_config.plugins.append(_OffsetPaginationSchemaPlugin())
488486

489487
if app_config.exception_handlers is None:
490488
app_config.exception_handlers = {}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Regression tests for SQLSpecPlugin's interaction with downstream plugins.
2+
3+
Litestar walks ``app_config.plugins`` with a generator expression that captures
4+
the list reference once. Plugins are allowed to register follow-on plugins by
5+
mutating that list in place (``.append`` / ``.extend``). If any plugin rebinds
6+
``app_config.plugins`` to a brand-new list, Litestar's iterator continues
7+
walking the old list and any plugin appended afterwards is silently dropped.
8+
9+
These tests pin SQLSpecPlugin to the in-place mutation contract.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import pytest
15+
from litestar import Litestar
16+
from litestar.config.app import AppConfig
17+
from litestar.plugins import InitPluginProtocol
18+
19+
from sqlspec.adapters.aiosqlite.config import AiosqliteConfig
20+
from sqlspec.base import SQLSpec
21+
from sqlspec.extensions.litestar.plugin import SQLSpecPlugin
22+
23+
pytestmark = pytest.mark.xdist_group("extensions_litestar")
24+
25+
26+
def _build_sqlspec_plugin() -> SQLSpecPlugin:
27+
sqlspec = SQLSpec()
28+
sqlspec.add_config(AiosqliteConfig(connection_config={"database": ":memory:"}))
29+
return SQLSpecPlugin(sqlspec=sqlspec)
30+
31+
32+
class _FollowOnPlugin(InitPluginProtocol):
33+
"""Marker plugin that records when its ``on_app_init`` fires."""
34+
35+
def __init__(self, fired: list[str], name: str) -> None:
36+
self._fired = fired
37+
self._name = name
38+
39+
def on_app_init(self, app_config: AppConfig) -> AppConfig:
40+
self._fired.append(self._name)
41+
return app_config
42+
43+
44+
class _AppendsFollowOnPlugin(InitPluginProtocol):
45+
"""Plugin that registers a follow-on plugin during its own ``on_app_init``.
46+
47+
Mirrors the ``litestar-vite`` ``VitePlugin`` -> ``InertiaPlugin`` pattern
48+
that originally surfaced this bug.
49+
"""
50+
51+
def __init__(self, follow_on: InitPluginProtocol) -> None:
52+
self._follow_on = follow_on
53+
54+
def on_app_init(self, app_config: AppConfig) -> AppConfig:
55+
app_config.plugins.append(self._follow_on)
56+
return app_config
57+
58+
59+
def test_on_app_init_preserves_plugins_list_identity() -> None:
60+
"""``app_config.plugins`` must be the same list object after ``on_app_init``.
61+
62+
Rebinding to a new list breaks Litestar's mid-iteration plugin discovery —
63+
the iterator captured the old list reference and silently skips anything
64+
a later plugin appends to ``app_config.plugins``.
65+
"""
66+
plugin = _build_sqlspec_plugin()
67+
app_config = AppConfig()
68+
original_plugins = app_config.plugins
69+
70+
plugin.on_app_init(app_config)
71+
72+
assert app_config.plugins is original_plugins, (
73+
"SQLSpecPlugin.on_app_init rebound app_config.plugins to a new list; "
74+
"this breaks downstream plugins that append to it during init."
75+
)
76+
77+
78+
def test_follow_on_plugin_fires_when_sqlspec_registered_first() -> None:
79+
"""[SQLSpec, Appender] order: appended follow-on plugin must still init."""
80+
fired: list[str] = []
81+
follow_on = _FollowOnPlugin(fired, "follow_on")
82+
appender = _AppendsFollowOnPlugin(follow_on)
83+
84+
Litestar(plugins=[_build_sqlspec_plugin(), appender])
85+
86+
assert "follow_on" in fired, (
87+
"Plugin appended to app_config.plugins during init was never called. "
88+
"SQLSpecPlugin must mutate app_config.plugins in place, not rebind it."
89+
)
90+
91+
92+
def test_follow_on_plugin_fires_when_sqlspec_registered_second() -> None:
93+
"""[Appender, SQLSpec] order: control case — must also fire."""
94+
fired: list[str] = []
95+
follow_on = _FollowOnPlugin(fired, "follow_on")
96+
appender = _AppendsFollowOnPlugin(follow_on)
97+
98+
Litestar(plugins=[appender, _build_sqlspec_plugin()])
99+
100+
assert "follow_on" in fired

0 commit comments

Comments
 (0)