Skip to content

Commit 04b133f

Browse files
authored
refactor: organize app.state into typed namespaces (#31)
* docs: app.state organization design Consolidate 15+ loose app.state attributes into one typed slot per owner (framework + each module), delete three duplicate locale fields, and drop runtime drift detection. Storage stays on app.state; only the shape changes. * docs: app.state organization implementation plan 14-task staged migration from loose app.state keys to typed slots (app.state.sm for framework + app.state.<module> for each module). Each task is a single revertable commit. * feat(core): add Services dataclass for framework singletons * test(core): match codebase test-class convention for Services * feat(hosting): populate app.state.sm alongside loose keys * refactor(hosting): read framework singletons via app.state.sm * refactor(hosting): read i18n + permission singletons via app.state.sm * refactor(host): read i18n state via app.state.sm with fallback * refactor(modules): read framework state via app.state.sm * feat(users): introduce UsersServices at app.state.users * refactor(users): read module state via app.state.users Migrate all users-module consumers from loose app.state keys (mailer, users_settings, rate_limiter, auth_throughput_limiter, event_bus, db) to the structured UsersServices slot introduced in Task 7. Update two test helpers that patched the old loose keys to patch app.state.users.* instead. * refactor(users): migrate bootstrap + its test helper to app.state.users/sm * test(users+auth): migrate remaining app.state reads to sm/users slots * refactor: delete deprecated loose app.state keys * refactor: migrate consumers to read from app.state.sm.* - Update conftest fixtures to access app.state.sm.db and app.state.sm.settings - Update get_db FastAPI dependency to use app.state.sm.db - Update test mocks and fixtures to use app.state.sm.* paths - Update dashboard stats and products deps to use app.state.sm.* - Update test assertions to verify sm instance instead of loose keys * fix: migrate remaining app.state reads missed in Task 10 * revert: restore generated i18n files (regen had stale fixture) * refactor: drop duplicate locale fields, update SM012, simplify i18n routes * refactor(hosting): plumb inertia_config via return value instead of app.state * docs+scaffold: adopt app.state.sm + app.state.<module> convention Add services.py.tpl template and register_settings scaffolding so new modules store settings inside a typed Services dataclass at app.state.<module_lower> rather than the old app.state.<module>_settings flat attribute. Update framework-conventions.md with Framework state and Module settings subsections reflecting the new pattern. * fix(verify): migrate remaining app.state reads in test fixtures + lint fixes * cleanup: simplify defensive guards + remove stale migration comments
1 parent b4b944c commit 04b133f

54 files changed

Lines changed: 2148 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎conftest.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def app(settings: Settings):
125125

126126
application = create_app(settings)
127127

128-
await _create_all_tables(application.state.db.engine)
128+
await _create_all_tables(application.state.sm.db.engine)
129129

130130
# Trigger lifespan startup so app.state.migration is populated
131131
ctx = application.router.lifespan_context(application)
@@ -146,7 +146,7 @@ async def client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
146146
requests from anonymous test flows (login, accept-invite, register) pass
147147
validation without first making a GET to mint a token."""
148148
signed = forge_session_cookie(
149-
app.state.settings.secret_key, {SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN}
149+
app.state.sm.settings.secret_key, {SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN}
150150
)
151151
transport = httpx.ASGITransport(app=app)
152152
async with httpx.AsyncClient(
@@ -163,7 +163,7 @@ async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
163163
"""HTTPX client with a signed session cookie carrying a seeded admin user's id."""
164164
from users.bootstrap import create_admin
165165

166-
async with app.state.db.session_factory() as session:
166+
async with app.state.sm.db.session_factory() as session:
167167
result = await create_admin(
168168
session,
169169
email="admin@test",
@@ -173,7 +173,7 @@ async def authenticated_client(app) -> AsyncGenerator[httpx.AsyncClient, None]:
173173
user_id = str(result.user.id)
174174

175175
signed = forge_session_cookie(
176-
app.state.settings.secret_key,
176+
app.state.sm.settings.secret_key,
177177
{"user_id": user_id, SESSION_CSRF_TOKEN_KEY: _TEST_CSRF_TOKEN},
178178
)
179179

‎docs/framework-conventions.md‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,34 @@ SM_DATABASE_URL, SM_ENVIRONMENT, SM_SECRET_KEY, SM_VITE_DEV_URL,
9292
SM_DEBUG, SM_LOG_LEVEL, SM_LOG_FORMAT, SM_MULTI_TENANT, SM_TENANT_HEADER
9393
```
9494

95+
### Framework state
96+
97+
Framework singletons live on `app.state.sm`, a frozen `Services` dataclass populated
98+
once at boot. Consumers read `request.app.state.sm.<field>` — never raw `app.state`
99+
attributes for framework-owned state.
100+
101+
Fields: `settings`, `db`, `event_bus`, `menu_registry`, `permissions`,
102+
`feature_flags`, `health_registry`, `i18n_registry`, `inertia_config`, `modules`.
103+
104+
Two attributes are intentionally kept outside `Services`:
105+
106+
- `app.state.inertia_dependency` — request-scoped `Depends` factory from fastapi-inertia.
107+
- `app.state.migration` — dev-only boot-time check result, set in lifespan.
108+
109+
### Module settings
110+
95111
Module settings should:
96112
- Use a per-module prefix: `SM_<MODULE>_*` (e.g. `SM_USERS_ALLOW_SIGNUP`).
97-
- Be stored on `app.state.<module>_settings` during `register_settings(app)`.
98-
- `SM012` diagnostic fires if `register_settings` is overridden but no `app.state.<module>_settings` is added.
113+
- Be stored inside a module-owned dataclass at `app.state.<module_lower>` during `register_settings(app)`.
114+
- `SM012` diagnostic fires if `register_settings` is overridden but no `app.state.<module_lower>` entry is added.
99115

100116
```python
101-
class AuthModule(ModuleBase):
117+
class UsersModule(ModuleBase):
102118
def register_settings(self, app: FastAPI) -> None:
103-
app.state.auth_settings = AuthSettings()
119+
from users.services import UsersServices
120+
from users.settings import UsersSettings
121+
122+
app.state.users = UsersServices(settings=UsersSettings())
104123
```
105124

106125
## Database

0 commit comments

Comments
 (0)