This is a pre-release. djust 1.0.0 has shipped since: read the djust 1.0.0 release notes.
Before you upgrade, read the upgrade guide.
Added
LiveView.abstract: bool = Falseclass-attribute marker — opt out abstract base classes from per-class V/Q system checks (#1605). A common pattern is to define an abstractBaseLiveView(LiveView)that subclasses extend for shared mount/auth boilerplate. The base typically has notemplate_nameand is never mounted directly, but V001 (missing template_name) and V005 (not inLIVEVIEW_ALLOWED_MODULES) still fired on it because the per-class check loop inpython/djust/checks.py:check_liveviewshad no abstract opt-out. The newabstract = Trueclass attribute mirrors Django'sMeta.abstractsemantics: setting it on a subclass skips that class's per-class V/Q checks (V001/V005/V002/V003/V004/V007/Q007), and the marker is consulted viacls.__dict__.get("abstract")so it is NOT inherited — subclasses of an abstract base are still validated as concrete unless they redeclareabstract = Truethemselves. Both the abstract opt-out and the globalsuppress_checksmechanism (see ### Fixed below) work; choose abstract when the intent is "this specific class is boilerplate" andsuppress_checkswhen the intent is "this check is the wrong shape for our codebase." Documented indocs/system-checks.md(new "Abstract base LiveView classes" section) anddocs/guides/error-codes.md(V001 + V005 entries). Behavior change: NEW public API —LiveViewgains anabstract: bool = Falseclass attribute. Existing user code sees no change unless it opts in.
Fixed
V008 no longer false-fires on stdlib module functions like
inspect.getsource,os.path.join,json.dumps,Path.read_text,datetime.isoformat(#1628). Follow-up to #1609/#1623. The bare-builtin fix in #1623 missed qualified calls because_get_call_namereturns the dotted name (inspect.getsource) for attribute-access calls — those didn't match the bare-nameSAFE_TYPESentries. Fix extendsSAFE_TYPESwith the cited qualified names:inspect.getsource/getsourcefile/getmodule/getdoc,os.path.join/basename/dirname/exists/isfile/isdir/abspath/relpath,os.getenv/getcwd,pathlib.Path.read_text/exists/is_file/is_dir,json.dumps,datetime.datetime.isoformat,datetime.date.isoformat. Also adds two bare method names (isoformat,read_text) for chained-call forms likePath(p).read_text()anddatetime.now().isoformat()where_get_call_namereturns just the method name. Bareexists/is_file/is_dirare intentionally NOT added (too ambiguous with user code —some_record.exists()etc.); use the qualifiedpathlib.Path.exists(p)form,os.path.exists(str(p)), or# noqa: V008. Reporter's alternative (annotation-based trust —-> strreturn annotation) is deferred — would require resolving imports + inspecting target module annotations at static-check time. Discovered buildingdjust-org/djust-starton djust 1.0.0rc12. After this lands, the starter has zero local accommodations for framework quirks. 10 new regression cases inpython/tests/test_checks_v008_stdlib_qualified_1628.py; 34 V008 tests green total (10 new + 9 from #1623 + 15 pre-existing). Gate-the-fix-off self-test (Action #1200/#1468) passes.{% code_block %}now syntax-highlights code blocks inserted via djust WS patches (#1625). The per-instance inline<script>that lazy-loads highlight.js worked on initial HTTP page load but failed for any<code>element that arrived via a WS patch — modern browsers don't execute scripts inserted viainnerHTML/DOM manipulation, so the inline highlight bootstrap never ran for re-inserted code blocks (they appeared plain-text). Fix installs a MutationObserver ONCE per page (gated bywindow.__djcHljsObserverInstalled) that watchesdocument.bodyfor added<pre><code class="language-*">elements and highlights any unmarked ones viahljs.highlightElement. The observer is installed on each of the three hljs-ready paths in the existing bootstrap (already-loaded, first-loads.onload, parallel-load poll), so it lives wherever the bootstrap can reach. Per-instance inline scripts still run on initial HTTP page load — the observer is purely additive. Feature-detected viatypeof MutationObserver === 'undefined'so very old browsers gracefully fall through.highlight=Falsepath unchanged. 6 new regression cases inpython/djust/components/tests/test_code_block_observer_1625.py(source-text gates pin the install + scope + selector + idempotency flag). Discovered buildingdjust-org/djust-starton djust 1.0.0rc12 — companion to #1624.{% theme_head %}now auto-loads djust-components'scomponents.csswhendjust.componentsis inINSTALLED_APPS(#1624). Previouslytheme_head(from djust-theming) loaded only djust-theming's owncomponents.css. djust-components ships a separatecomponents.cssatpython/djust/components/static/djust_components/components.csswith layout rules for{% code_block %},{% card %},{% dj_button %}spinners, etc. — buttheme_headdidn't link it, so components rendered in user templates fell back to default flow layout (looked broken). Fix detectsdjust.componentsviadjango.apps.apps.is_installed("djust.components")inbuild_theme_head_contextand adds a conditional<link>next to the existing djust-theming link intheme_head.html. Detection is defensive —apps.is_installed()raises if the app registry isn't populated yet, so the call is wrapped in try/except and falls back to no link. Withoutdjust.componentsinstalled, theme_head emits no extra link (graceful degradation, zero behavior change for users not on djust-components). 6 new regression cases inpython/djust/tests/test_theme_head_components_link_1624.py; the#1123-style pre-mount/post-mount keyset invariant test (TestThemeMixinThemeHead::test_build_theme_head_context_keyset) updated to include the new context key. Discovered buildingdjust-org/djust-starton djust 1.0.0rc12. Gate-the-fix-off self-test (Action #1200/#1468) passes.V008 no longer false-fires on stdlib primitive-returning builtins (#1609). V008 (
Non-primitive type assigned to self.X in mount()) inspected the bare call name against aSAFE_TYPESset that contained type-constructor names (list,dict,str,int, ...) but missed stdlib builtins that always return primitives. Result:self.online_count = max(1, len(...))triggered the warning even thoughmax(int, int)returns an int. Fix extendsSAFE_TYPESwith numeric builtins (max,min,sum,abs,round,pow,divmod,len,ord,hash,id), string-conversion builtins (bin,oct,hex,repr,chr,ascii,format),sorted(returns list, same element-serializability trust contract aslist()), andfrozenset/bytes(overlooked scalar/container primitives). Iterator-returning builtins (reversed,enumerate,zip,map,filter,range,iter) intentionally remain flagged — they return iterator/generator objects that aren't directly JSON-serializable when stored on a view; the user must materialize vialist()first.complexandslicealso remain flagged. The V006 (Warning) path is untouched. Discovered buildingdjust-org/djust-starton djust 1.0.0rc7. 9 new regression cases inpython/tests/test_checks_v008_builtins_1609.py; 15 existing V008 tests atpython/tests/test_checks.py::TestV008NonPrimitiveInMountcontinue to pass unchanged. Gate-the-fix-off self-test (Action #1200/#1468) passes.dj_button(variant="danger")now renders styled (#1619).dj_buttonpreviously producedclass="btn btn-danger"unconditionally from thevariantkeyword, but djust-theming'scomponents.css(loaded bytheme_head) only ships rules for.btn-primary,.btn-secondary,.btn-destructive,.btn-ghost, and.btn-link— sovariant="danger",variant="success", andvariant="warning"rendered with class names that had no matching CSS rule. (scaffold.cssDOES have.btn-danger/.btn-successrules but is not loaded bytheme_head.) Fix introduces a_DJ_BUTTON_VARIANT_CLASS_MAPinpython/djust/components/templatetags/djust_components.pymapping keyword variants to the canonical CSS class names;dangeris now an alias fordestructive(matching shadcn/Tailwind convention). Variants not in the map (including the now-deprecatedsuccess/warningkeywords, plus user-defined custom variants) pass through asbtn-<variant>viaconditional_escape, preserving the existing security boundary and enabling user theme classes. Thedangerkeyword alias keeps back-compat with existing templates (e.g.,python/djust/components/gallery/examples.py:158). Docstring updated to list the 5 supported variants. Discovered buildingdjust-org/djust-starton djust 1.0.0rc12. 5 new regression cases inpython/djust/components/tests/test_dj_button_variant_1619.py(danger alias, destructive canonical, primary unchanged, unknown passthrough, XSS-escape preserved); gate-the-fix-off self-test (Action #1200/#1468) passes.Render diff misrouted SetText patches when a template variable was adjacent to literal text (#1617).
build_fragment_text_map(crates/djust_live/src/lib.rs:2597-2633) mapped each rendered fragment to the first VDOM text node whose content equalled the fragment. For{{ online_count }} online, the variable's rendered fragment ("1") doesn't equal the chip's full text content ("1 online"), so the matcher fell through to a sibling text node whose content happened to equal"1"(typically a bare reaction count). When the variable changed, theSetTextpatch landed on the wrong node — chip stayed at"1 online"forever while the unrelated reaction count visually became"2"(state still said1). Fix maps each fragment by its byte position in the assembled HTML to the text node whose HTML range contains it, claiming the entry only when the fragment IS the entire text node (full-coverage check). Ambiguous cases (partial-overlap, whitespace-only fragments, fragments containing tags) fall through to the byte-leveltext_region_fast_path, which is already sound for this scenario. Bug class: any{{ var }}<literal>,<literal>{{ var }}, or{{ a }}{{ b }}template pattern. The reporter'sstate= works / handler= brokenframing was refuted by code inspection: bothpush_to_viewpaths converge at_sync_state_to_rust→set_changed_keys→render_with_diffand take the sametext_fast_path; the fix addresses the root cause. Discovered buildingdjust-org/djust-starton djust 1.0.0rc12. 3 new regression cases inpython/djust/tests/test_text_fast_path_misroute_1617.py(bug repro, adjacent{{a}}{{b}}, pure-case regression backstop); 6 existing#1529content-collapse regression cases still pass; wire-protocol invariants (Actions #1448/#1538/#1541) preserved — SetText struct + msgpack/JSON serialization unchanged. Gate-the-fix-off self-test (Action #1200/#1468) passes: reverting to content-equality matching reproduces the misroute, restoring the position-aware body makes it pass.WS-mount HTML now properly applied to pre-rendered DOM (#1610). When the client signaled
has_prerendered=truein the WS mount message, the server's WS-mount HTML was previously used ONLY to stampdj-idattributes onto the existing pre-render DOM (_stampDjIds(data.html)atpython/djust/static/djust/src/03-websocket.js:361). Any state that diverged between HTTP-prerender and WS-mount context — presence counts,_websocket_session_id-derived values, anything that only resolved in the WebSocket scope — was silently dropped, and the DOM stayed at the prerender values until a subsequent broadcast happened to mutate something else. Fix callsmorphChildren(the same helper used byhandleEmbeddedUpdateat03-websocket.js:1127and thehtml_recoverypath at03-websocket.js:641) to diff the pre-render DOM against the WS-mount HTML and apply the differences.morphChildrenpreserves keyed nodes by id, so the dj-id stamp step is folded into the morph. PR #1615'strack_presence/untrack_presenceauto-broadcast partially masked this bug for the specificonline_countcase (the broadcast synthesized a patch frame post-mount); this fix closes the general bug class for non-presence WS-context state. Discovered buildingdjust-org/djust-starton djust 1.0.0rc7. 8 new JS regression cases intests/js/ws-mount-prerender-divergence-1610.test.js(source-text gate + JSDOM live tests + sticky-exclusion + missing-container fallback) plus 3 server-side correctness pins inpython/djust/tests/test_ws_mount_prerender_divergence_1610.py. Gate-the-fix-off self-test (Action #1200/#1468) passes: reverting themorphChildrencall to_stampDjIds(data.html)makes the JSDOM live tests fail, restoring it makes them pass.DJUST_CONFIG = {"suppress_checks": [...]}now silences V002, V003, V004, V007, and Q007 (#1607). Direct mechanical follow-up to #1604 — the same wiring oversight, on five additional check IDs that share the per-class loop inpython/djust/checks.py::check_liveviews. V002 (nomount()method), V003 (wrongmount()signature), V004 (handler-like name without@event_handler), V007 (event handler missing**kwargs), and Q007 (overlappingstatic_assigns∩temporary_assigns) all emitted warnings without consulting the project-wide_is_check_suppressed()helper. With this PR every V/C/T/Y/Q emission site inside the per-class loop now honors the globalDJUST_CONFIG['suppress_checks']shortcut; the per-classabstract = Trueopt-out from #1605 already covered abstract classes but the global-by-ID escape hatch was missing. 10 regression cases inpython/tests/test_checks_1607_suppress.py(suppress + regression per ID); gate-the-fix-off self-test (Action #1200/#1468) passes for each — reverting an individual guard makes the corresponding suppress test fail, restoring makes it pass.DJUST_CONFIG = {"suppress_checks": ["V001", "V005"]}now silences V001 and V005 (#1604). V001 (python/djust/checks.py:1215-1244) and V005 (python/djust/checks.py:1382-1393) emitted warnings without consulting the project-wide_is_check_suppressed()helper that every other V/C/T/Y check (C003, C013, C014, C303, V008, V010, V011, Y001-4, T002, T012, ...) already honored. Result: the documented escape hatchDJUST_CONFIG = {"suppress_checks": ["V001", "V005"]}was silently a no-op for V001/V005 even though it worked for C003 (the original reporter's confusion). Fix wraps both emission sites with_is_check_suppressed("djust.V001")/_is_check_suppressed("djust.V005")guards matching the existing pattern. Discovered while building thedjust-org/djust-startstarter template, which ships aBaseLiveViewpattern that hits both #1604 and #1605. Reporter'sSILENCED_SYSTEM_CHECKS = ["djust.V001", "djust.V005"]workaround (Django's own mechanism) still works. Hint text for both checks updated to mention all three escape hatches (abstract = True,DJUST_CONFIG['suppress_checks'], andSILENCED_SYSTEM_CHECKS). 9 regression cases inpython/tests/test_checks_1604_1605.pylock both fixes in (4 suppression cases, 4 abstract cases including non-inheritance and explicit-False, 1 base-class declaration check); gate-the-fix-off self-test (Action #1200/#1468) passes — reverting the V001 guard makestest_v001_suppressed_via_djust_configfail, restoring it makes it pass.