djust 0.9.6rc2

Pre-releaseReleased

This is a pre-release. djust 0.9.6 has shipped since: read the djust 0.9.6 release notes.

Performance

  • theme_context now pre-renders theme_panel, theme_mode_toggle, and theme_preset_selector as 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's base.html had {% 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_context now caches its rendered output by ThemeState tuple (#1437). The Django context processor djust.theming.context_processors.theme_context previously ran the full CSS-generation + theme-switcher HTML pipeline on every templating request. Output is now lru_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. New djust.theming.context_processors.clear_theme_context_cache() exposed for theme-pack hot-reload and tests.
  • TenantMiddleware short-circuits when no resolver is configured (#1436). When neither DJUST_CONFIG['TENANT_RESOLVER'] nor DJUST_TENANTS is set, the middleware now bypasses the resolver call, the thread-local set/clear pair, and the required-tenant gate — switching __call__ to a straight get_response(request) passthrough. Saves ~2-5% per-request CPU for consumers with djust[tenants] installed but no tenant opt-in (single-tenant deploys, scaffold starters, demo apps). Consumers who set either config keep the full path; request.tenant is still set to None on the no-op path so getattr(request, "tenant", None) callers see the same shape.

Added

  • System check djust.D001 — warn when Postgres is configured but psycopg[binary]>=3.2 is not installed (#1433). djust's db.notifications (LISTEN/NOTIFY bridge) requires psycopg3. The 0.9.5 cycle hardened the runtime path to permanent-fail with a WARNING when @notify_on_save actually fires (#1357), but operators who deploy the misconfig without an active consumer wouldn't see the warning until much later. D001 surfaces it at manage.py check / runserver startup, before traffic. Fires only when the default DB engine is Postgres AND psycopg2 is importable AND psycopg (3.x) is missing or at version < 3.2. Silenceable per-project via SILENCED_SYSTEM_CHECKS = ['djust.D001'].

Fixed

  • RedisStateBackend now uses per-thread ZstdCompressor/ZstdDecompressor (#1430).zstandard.ZstdCompressor and ZstdDecompressor are NOT thread-safe (python-zstandard #244, closed "by design"). The previous RedisStateBackend.__init__ stored a single instance of each on self, so concurrent callers raced on the C-level state. Symptoms ranged from ZstdError("Unknown frame descriptor") and "Data corruption detected" to outright SIGSEGV inside ZSTD_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 a threading.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). When RustLiveView.deserialize_msgpack raised — 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 a cargo build of djust_vdom + .so swap, fresh navigations could re-render with state from the prior session. Now the backend pops the corrupt entry from its in-memory dict and returns None; the caller's mount path treats the cache as cold and runs mount() cleanly. Discovered during the #1408 investigation.

All releases · Atom feed