This is a pre-release. djust 0.9.6 has shipped since: read the djust 0.9.6 release notes.
Performance
theme_contextnow pre-renderstheme_panel,theme_mode_toggle, andtheme_preset_selectoras context strings (#1435). Templates can now use{{ theme_panel }}/{{ theme_mode_toggle }}/{{ theme_preset_selector }}instead of the corresponding{% … %}tags. The work runs once per request in the context processor instead of once per{% … %}invocation — meaningful when the same tag appears multiple times on a page (e.g., djust-scaffold'sbase.htmlhad{% theme_panel %}twice). Customization-with-args still uses the{% … %}form. If a tag function raises (broken manifest, missing template, downstream shadowing), pre-renders come back as empty strings instead of 500-ing the request.theme_contextnow caches its rendered output byThemeStatetuple (#1437). The Django context processordjust.theming.context_processors.theme_contextpreviously ran the full CSS-generation + theme-switcher HTML pipeline on every templating request. Output is nowlru_cache(maxsize=512)'d on(theme, preset, pack, mode, resolved_mode, layout, presets_key)— a pure function of state, no request data flows in. djust's catalog of ~60 presets × 2 modes × handful of packs fits comfortably under the cache size. Per-request cost: ~1-3 ms → ~5-10 µs post-warmup. Newdjust.theming.context_processors.clear_theme_context_cache()exposed for theme-pack hot-reload and tests.TenantMiddlewareshort-circuits when no resolver is configured (#1436). When neitherDJUST_CONFIG['TENANT_RESOLVER']norDJUST_TENANTSis set, the middleware now bypasses the resolver call, the thread-local set/clear pair, and the required-tenant gate — switching__call__to a straightget_response(request)passthrough. Saves ~2-5% per-request CPU for consumers withdjust[tenants]installed but no tenant opt-in (single-tenant deploys, scaffold starters, demo apps). Consumers who set either config keep the full path;request.tenantis still set toNoneon the no-op path sogetattr(request, "tenant", None)callers see the same shape.
Added
- System check
djust.D001— warn when Postgres is configured butpsycopg[binary]>=3.2is not installed (#1433). djust'sdb.notifications(LISTEN/NOTIFY bridge) requires psycopg3. The 0.9.5 cycle hardened the runtime path to permanent-fail with a WARNING when@notify_on_saveactually fires (#1357), but operators who deploy the misconfig without an active consumer wouldn't see the warning until much later. D001 surfaces it atmanage.py check/runserverstartup, before traffic. Fires only when the default DB engine is Postgres ANDpsycopg2is importable ANDpsycopg(3.x) is missing or at version < 3.2. Silenceable per-project viaSILENCED_SYSTEM_CHECKS = ['djust.D001'].
Fixed
RedisStateBackendnow uses per-threadZstdCompressor/ZstdDecompressor(#1430).zstandard.ZstdCompressorandZstdDecompressorare NOT thread-safe (python-zstandard #244, closed "by design"). The previousRedisStateBackend.__init__stored a single instance of each onself, so concurrent callers raced on the C-level state. Symptoms ranged fromZstdError("Unknown frame descriptor")and "Data corruption detected" to outright SIGSEGV insideZSTD_decompressSequencesLong_default(reproduced on Linux 4.14 + Python 3.12 + zstandard 0.25.0; the segfault took down a microVM in production). Both objects now live in athreading.local, accessed lazily via_get_compressor()/_get_decompressor(). Each thread gets its own instance — no shared state, no race. Per-thread instances are reused within a thread (no per-call construction overhead).InMemoryStateBackend.get()discards corrupt entries instead of returning the shared in-memory ref (#1410). WhenRustLiveView.deserialize_msgpackraised — typically after a hot-swap struct change or msgpack schema drift — the previous fallback returned the cached object directly. Two concurrent connections to the same view then shared one_rust_view, and mutations from connection A leaked into connection B's render context. After acargo buildofdjust_vdom+.soswap, fresh navigations could re-render with state from the prior session. Now the backend pops the corrupt entry from its in-memory dict and returnsNone; the caller's mount path treats the cache as cold and runsmount()cleanly. Discovered during the #1408 investigation.