Polish release on top of 0.9.0. Five drain buckets shipped between the 0.9.0 GA bump and this tag (0.9.1-1 through 0.9.1-5 under the new SemVer-pre-release-suffix milestone naming convention adopted 2026-04-30; equivalent to historical v0.9.1/v0.9.2/v0.9.3/v0.9.4/v0.9.5 drain buckets under the old naming). Headlined by a real-bug VDOM fix (#1205), a broadcast-recovery fix (#1202), the Debug Panel UI (#1151), and a RichSelect ergonomics expansion (#1204).
Added
RichSelect variant support, trigger tinting, and onclick parity (#1204). Each option dict accepts an optional
variantkey that tints the row in the dropdown AND the trigger when that option is currently selected. Built-in variants align withBadge/Button/Tag/Alertvocabulary (info,success,warning,danger,muted,primary,secondary). Newvariant_mapkwarg mirrorsBadge.status()for value→variant mapping cases. Permissive variant-name regex (^[a-z0-9][a-z0-9-]{0,31}$) lets downstream projects ship custom variants by adding a matching.rich-select-option--variant-<name>CSS rule. Trigger now emits the open/closeonclickhandlers that{% rich_select %}template tag always emitted, eliminating the monkey-patch-rendered-HTML workaround programmatic consumers used to need.LiveViewTestClient.render_with_patches()— VDOM-diff accessor for tests (#1208). New public method ondjust.testing.LiveViewTestClientthat wrapsview_instance.render_with_diff()and returns(html, patches_list, version)with the JSON patches parsed into a Python list. Empty list when no patches were produced. Reusable for any test that needs to assert on VDOM-diff invariants (e.g. "this noop event must produce zero patches"). First user is the strengthenedtest_normalize_idempotent_on_already_serializedregression test intests/unit/test_list_model_diff_1205.py, which now locks the #1206 normalize-pass idempotency contract via an explicitpatches == []assertion instead of the prior weaker "no exception" check.
Fixed
JIT serializer silently degrades when context value is
list[Model](#1205, expanded by #1207). When a view'sget_context_dataoverride setsctx["tasks"] = list(qs)after callingsuper().get_context_data(), the JIT auto-serialization pipeline runs insidesuper()and never sees the user-added value. The rawlist[Model]then flows through_sync_state_to_rust, where change-detection compares list elements via Python==— which delegates toModel.__eq__(pk-only). In-place field mutations (is_activetoggle,completedflip) don't changepk, so the comparison returns equal, the key is never added to the diff-context, Rust never receives the new state, and the rendered HTML is byte-identical on every event despite confirmed DB writes. Symptom from the issue reporter:patch_count: 0on every event,_debug.variables.tasks.valueshows only__str__strings.Initial fix (#1206):
_sync_state_to_rust(inpython/djust/mixins/rust_bridge.py) now runs a defensive normalize pass overfull_contextimmediately after fetching it, converting any homogeneouslist[Model]/Model/QuerySetvalue to dicts vianormalize_django_value. After normalization, change-detection compareslist[dict] != list[dict]element-wise viadict.__eq__(structural), correctly catching field mutations. Idempotent on already-serialized values. Also removed dead_lazy_serialize_contextmethod frompython/djust/mixins/jit.py(zero call sites — was misleadingly cited as the bug location in the issue).Shape coverage expansion (#1207): the initial fix only handled homogeneous
list[Model]; PR-review surfaced two more shapes that escape change-detection — heterogeneous[dict, Model](Model not first;is_model_listchecked onlyvalue[0]) and nestedlist[list[Model]](grouped tasks). Refactored the inline normalize loop into a recursive_normalize_db_valueshelper that scans the full list for any-position Model and recurses into nested lists with bounded depth (_NORMALIZE_DEPTH_LIMIT = 3). 9 regression cases intests/unit/test_list_model_diff_1205.pylock down all shape variants: homogeneouslist[Model], singleModel, rawQuerySet, heterogeneous[dict, Model], nestedlist[list[Model]], idempotency, empty list, and mixed-type list.Broadcast renders now refresh
_recovery_html(#1202).server_pushinpython/djust/websocket.pywas sending broadcast patches without updatingself._recovery_html/self._recovery_version(the user-initiated event path did this; broadcasts were overlooked). Consequence: when a subsequentapplyPatcheson the client failed (the well-known{% if %}-shifts-DOM case), the client sentrequest_htmlexpecting fresh HTML, buthandle_request_htmlread a stale orNone-valued_recovery_htmland returnedrecoverable: false. The client then triggered a full-page reload. Sessions that only received broadcasts after mount (admin dashboards, real-time apps with Celery pushes) never populated_recovery_htmlvia the normal path. PR #1203 mirrors thehandle_eventrecovery-state-store atwebsocket.py:3271: before sending broadcast patches,server_pushnow stores the render output as_recovery_html/_recovery_version. Two regression cases intests/unit/test_server_push.pylock the store-after-broadcast invariant + the no-patches branch.
Process & tooling (internal)
The v0.9.1 arc shipped a substantial body of internal-tooling work that doesn't change user-facing API but improves contributor and maintainer ergonomics. Highlights for the audit trail (no migration needed):
- Pre-push lints:
scripts/check-no-dead-private-methods.py(#1209),scripts/check-no-comma-list-closes.py(#1227),scripts/audit-pipeline-bypass.py(#1212). - Pipeline-template Stage 4 reproducer-first mandatory item (#1210), Stage 11 reviewer-prompt budget guidelines (#1211), and the two-commit shape canonicalization + 3-clean-runs verification (Action Tracker #181/#182) as structural gates.
- CodeQL sanitizer model for
djust.security.log_sanitizer.sanitize_for_log(#1214) — closes the FP class PR #1201's 8 dismissals worked around. - CLAUDE.md "Bug-report triage" section citing PR #1206 as the canonical case study for issue-reporter-analysis-not-equal-root-cause discipline (#1213).
- 20+ retro patterns canonicalized across CLAUDE.md sections for v0.6.x–v0.8.x retro arcs (#1226), v0.9.4 retro arc (#1225).
- Pre-commit
.pxdexclude prevents binary archive corruption (#1215). - Hot-reload auto-enable via
DjustConfig.ready()in DEBUG mode (#1190). - Debug Panel UI for time-travel + forward-replay (#1151 / PR #1194, on top of v0.9.0's wire-protocol foundation).