Before you upgrade, read Removed below.
The third 1.3 release candidate. It lands the rest of the component-conventions arc: ADRs 034–037 (#3122).
- ADR-034: interactive components own their behavior.
djust.components.interactivestarts with a dropdown, and keyed collections of them work. - ADR-035:
djust.forms.ModelFormMixinedits one authorized object with a DjangoModelForm. - ADR-036: typed event parameters. There is a staged
strictparameter policy; the default stayslegacy. - ADR-037:
manage.py checknow compares every template event binding with its handler (djust.T019–T022), using the same handler discovery as the runtime. - Other:
djust.worker_pool.PooledHTTPruns HTTP requests on a bounded thread pool (#3114). SSE and the HTTP fallback now behave like the WebSocket path.
Upgrade notes. Most apps need no changes. Check these:
manage.py checkcan now fail your build.djust.T019–T022are warnings. A project that runscheck --fail-level WARNING(or--deploywith warnings treated as errors) will fail on any binding the checks flag. Fix the binding, or suppress a deliberate one with{# noqa: T019 -- <reason> #}on the line or the line above.- HTTP-fallback events are sent one at a time, in order. Before, two could be in flight at once, and a form save could store stale values. Pages that fire several events quickly will see them queue.
- A zero-patch HTTP render returns
{"patches": []}with the new version instead of resetting the diff baseline, which reloaded the page. Custom clients of the HTTP event endpoint should accept an empty patch list. dj-pasteroutes to the component or embedded child it is in, like the otherdj-*events. It no longer goes to the parent view. A parent handler that relied on receiving a child's paste must move to the child.- Check IDs.
djust.C021is theworker_threadscheck (from rc2). An invalidevent_parameter_policyisdjust.C022. Silence it under that ID. djust.V007("event handler missing**kwargs") is retired. Remove it fromSILENCED_SYSTEM_CHECKSif you had it there.
Added
djust.worker_pool.PooledHTTP: run HTTP requests on a bounded pool of threads (#3114). Django's ASGI handler gives every HTTP request a new thread. On free-threaded CPython each thread's allocator heap stays resident after the thread exits, so an overload burst of page loads left the process at its peak thread count's memory for good: 256 concurrent snake-arena page GETs took RSS from 81 MB to 1.26 GB with an 89 MB live Python heap. Wrap the HTTP app,"http": PooledHTTP(get_asgi_application()), and each request runs on one of a small pool of long-liveddjust-http-Nthreads instead; the rest wait on the event loop.threads=NonefollowsLIVEVIEW_CONFIG["worker_threads"]and passes through while that is off. Opt-in: nothing changes unless you wrap the app. In the snake-arena overload run (64 → 192 → 256 clients, then idle, 3.14t,worker_threads=5) peak and after-idle RSS fell from 2030 MB to 936 MB, and the 256-client p95 event round trip from about 7 s to 0.33 s. The scaling guide gains a "Memory under overload" section: RSS per client, what the allocator keeps, and which queues are bounded. Tests inpython/djust/tests/test_overload_memory_3114.py.Keyed collections of interactive dropdowns (ADR-034 C3).
rows = DropdownMenu.collection()declares one.self.rows.sync([(key, DropdownMenu(...)), ...])reconciles its members:- retained keys keep their state;
- reordering never moves state between rows;
- removed keys are refused afterwards, and a re-added key is a new lifetime;
- duplicate keys change nothing.
One
@rows.on.selectedcallback receives the member that emitted, withcomponent.keyits collection key.get(key),len(), iteration and.valuesgive the current members.A view with a collection is mounted fresh on Back navigation, so a member removed since cannot come back.
djust.components.interactive: a dropdown that owns its behavior and reports typed outputs (ADR-034 C1).DropdownMenu(label=..., items=[...])opens, closes and validates a selection itself, then calls the view's@menu.on.selected def ...(self, component, value: str)callback.- Items are typed as
ActionItem/SeparatorItem.visibility="client"hands open/close to the browser's native popover, and an optional@menu.on.toggledcallback observes it. - Each view instance gets its own component state. Events route only through the server's registry. mypy and Pyright catch misspelled menus, unknown outputs and wrong callback signatures.
- Two new checks:
djust.V020(interactive components onuse_actorsviews, which 1.3 does not support) anddjust.Q004(a module importing both this and the legacyDropdownMenu).
djust.forms.ModelFormMixin: edit one authorized object without writing a custommount()(ADR-035 F1). Declaremodel(orget_queryset()), aModelFormasform_class, and route the view with apkorslug.- Lookup and authorization. Before the form is built, the object is looked up in
get_queryset()and authorized withhas_object_permission(). Every later event does both again. - Denials. A missing, filtered-out or forbidden object is the same permission denial on every transport, and no form or hook runs for it.
- Where the id comes from.
self.kwargsis the route's resolved kwargs on HTTP, WebSocket and SSE. Client mount parameters never select the object. self.object. It is the object authorized for the current request or event, and is looked up once per mount or event. It renders asobject, plus an opt-incontext_object_name, and is never persisted.self.object = form.save()works; assigning any other record raisesValueError.- Typing.
ModelFormMixin[Project]typesself.object. - New check.
djust.S013warns when an adapter view overrides neitherget_queryset()norhas_object_permission().
FormMixinand its_model_instancepattern are unchanged.- Lookup and authorization. Before the form is built, the object is looked up in
Staged ADR-036 contracts reach HTTP pages. A page whose view has strict-policy handlers now renders its public parameter contracts into a JSON data block outside the live root. HTTP-fallback render responses carry the rendered tree's contracts, the same fields as WebSocket/SSE render frames. The client keeps them as the page's own scope and resolves a native binding's owner (root, component or embedded child) and handler against the transport it will send through. Native binders do not consume the contracts yet. All-legacy pages and responses are unchanged. 10 collected cases in
python/djust/tests/test_http_parameter_contracts.py.Startup checks for the staged ADR-036 strict parameter policy. A strict-policy declaration that strict dispatch would reject is now reported by
manage.py check, naming the view, the handler and the parameter:djust.C022for an invalidevent_parameter_policy,djust.V016for an unresolvable or unsupported annotation, a missing annotation or a reserved argument name,djust.V017for an async strict handler on an actor view, anddjust.V018whenparams=disagrees with a strict signature. The checks compile the same cached contract dispatch uses. Deferred annotations now resolve against the defining class body before module globals, and strict contracts reject keyword parameters namedview_id,component_idor starting with_, which no transport can deliver. V007's "add**kwargs" advice no longer applies to strict handlers. Legacy-policy handlers, still the default, report nothing new. 32 collected cases inpython/djust/tests/test_parameter_contract_checks.py.Native event binders honour the staged ADR-036 strict parameter policy. For a strict handler,
dj-click, the form directives, keyboard, paste, polling, scoped window/document, click-away, shortcut, mouse,dj-mounted, form-recovery, dropdown-observation, JSpushanddj-viewportbindings senddj-value-*arguments, parsed strictly, plus only the generated values (value,field, form fields,key...) the handler declares, or all of them for a**catch-all._targetis not sent under strict. A malformed typed literal, a wire hint the declared type rejects, adj-value-*key reusing a generated name, or a value given both positionally and by name is rejected in the browser. That happens before any lock, disable-with, optimistic or loading effect, through the existing value-freedjust:errorpath. Legacy handlers keep their exact payloads.manage.py checkcompares template event bindings with their handlers (ADR-037):djust.T019–T022. Each LiveView and LiveComponent template is compiled, not rendered, with{% extends %}and constant{% include %}followed. Every literaldj-*binding is checked against its owner using the runtime's own handler discovery, parameter policy and strict contract:T019: a missing, undecorated or wrongly owned handler;T020: missing, unexpected or duplicated arguments;T021: literals and wire hints the handler rejects;T022: routing context in markup.
All four are Warnings in 1.3.
djust_check --format jsonadds acoverageobject (checked, dynamic and unsupported bindings, with gaps). Its binding findings carryowner,binding,expectedandsupplied. Suppression is local and needs a reason:{# noqa: T019 -- <reason> #}. 23 cases inpython/djust/tests/test_adr037_binding_checks.py.
Changed
Less work on the asyncio event loop per WebSocket frame (#3095). On free-threaded CPython with
LIVEVIEW_CONFIG["worker_threads"]the event loop, not the cores, caps one process. A profile of a multi-room game at saturation showed the loop spending about a third of its time on events that mostly skip the render. Changes that apply everywhere:- no thread hop for the handler-permission check when the handler has no
@permission_required, nor for the object-permission check when the view does not overrideget_object; both are metadata checks then; - a handler's
inspect.signatureand type hints are resolved once per function, and again if its code, defaults or annotations change (a failed type-hint resolution is retried, as before); - a free render lock is taken without arming an
asyncio.wait_fortimer; djust.layers.InMemoryChannelLayer.group_senddelivers without creating a task per member.
With the worker pool on only: Channels' (4.2+) per-frame
close_old_connectionshop is replaced by a check the session's pool thread runs before its next task, and a tick's change-detection snapshots run in thehandle_tickhop. A deferred check that raises is logged (without the error's text) and the task goes on; the task's own database access then reports a broken connection. 24 regression tests inpython/djust/tests/test_event_loop_ceiling_3095.py.- no thread hop for the handler-permission check when the handler has no
Presence broadcasts respect
push_scope(#3095). A join or leave in aPresenceMixinview pushed_on_presence_changeto every session of the view, so in a multi-room view one join woke every session in every room. When the view setspush_scope, the broadcast now reaches only the sessions that share the sender's presence key: every WebSocket session of such a view joins a per-key group, including sessions that never calltrack_presence()(their key follows theirpush_scope), and a session whose count went stale between mount and that join refreshes once. The new class attributepresence_broadcast_scopedchooses explicitly (True: scoped withoutpush_scope;False: the view-wide broadcast).Behaviour change — who is affected: views that set
push_scope(new in 1.3) and override_on_presence_changeto react to joins and leaves under other presence keys. The default handler only recounts its own key, so its result is unchanged. Migration: setpresence_broadcast_scoped = False. Views withoutpush_scopeare unchanged: they join no extra group. 19 regression tests inpython/djust/tests/test_presence_scoped_broadcast_3095.py.dj-auto-recoverhandlers always run under the legacy parameter policy (ADR-036 decision R1). A handler that adj-auto-recoverbinding targets is dispatched, and advertised to the browser, as legacy, even in a project using the staged strict policy, so its_form_values/_data_attrsdictionaries keep working. Targets are read from the HTML the server rendered (including{% include %},{% extends %}, conditional and dynamic bindings), so a client cannot claim the downgrade. An explicitparameter_policy="strict"on such a handler is reported at startup as the warningdjust.V019. 12 collected cases inpython/djust/tests/test_recovery_handler_policy.py.The staged strict collector refuses
_-prefixeddj-value-*names. They match the server's reserved-name rule for strict parameters (ADR-036 D5), so the browser rejects them before sending instead of relying on a server rejection. A 43-row conversion and binding matrix now runs identically through every server path inpython/djust/tests/test_strict_transport_parity.py: the shared runtime, real WebSocket (normal and actor), real SSE, both HTTP-fallback shapes, the exposed API and the test client.One dispatch-context rule for the staged ADR-036 strict parameter policy. A strict handler now receives the same application arguments whichever transport delivered the event. The transport keys
_cacheRequestIdand_activityare dropped before binding. Before, a strict event carrying either one was rejected over the HTTP fallback, the exposed API, server functions, the test client, replay and actor views. An unconsumedview_idorcomponent_idfails closed instead of reaching the root handler. Unknown_keys in the flat HTTP body are rejected instead of silently discarded. Strict contracts can declare framework-supplied (trusted) parameters that no client key or positional value can fill, and the staged ADR-034 output callbacks now bind their payload through that contract with the source component supplied by the framework. Legacy handlers are unchanged. 68 collected cases inpython/djust/tests/test_trusted_dispatch_context.py.The MCP tool
find_handlers_for_templateuses themanage.py checkbinding scan (ADR-037). A view or component now matches when its template is the file, or includes or extends it, instead of when the file names match. The existing JSON keys are unchanged. Each view gainsbindings(with each binding'sstatusanddjust.T019–T022findings), and the response gains acoverageobject. 5 cases inpython/djust/tests/test_find_handlers_for_template.py.dj-auto-recovertargets are found in included and parent templates too (ADR-037). The class-level scan that forces recovery handlers onto the legacy parameter policy (ADR-036 decision R1) now uses the template binding scan. It follows{% include %}and{% extends %}and keeps every{% if %}branch, so more handlers are legacy-forced from mount. For example, a recovery form inside a conditional include was strict until an event rendered it; now it is legacy from mount. Computed targets are still seen only in the render. 17 cases inpython/djust/tests/test_recovery_handler_policy.py.LiveViewSmokeTestfuzzes exactly the handlers dispatch resolves (ADR-037). It no longer fuzzes undecorated public methods. With the defaultevent_security = "strict"the server refuses to call them; under"warn"or"open"they are still callable but are no longer fuzzed, so decorate them (or test them directly) to keep that coverage. So a smoke suite sends fewer events, and a failure it reported on such a method no longer appears. A strict-policy handler is fuzzed with its contract's parameter metadata. Components and server functions stay excluded.CodeQL code-scanning sweep. Fixed every open non-cycle alert:
- A strict handler's parameter-validation error reaches the client from
ParameterError.public_messageinstead ofstr(exc)(the text was already framework-written and value-free). - Removed an unreachable explicit-child branch in
{% live_render %}and a dead variable in the gallery catalogue. - The service-worker mount-metadata hook moved onto
djust._swasapplyMountMetadata, so neither transport needs atypeofguard. ServerStateSessionderives its storage key through an overridable_storage_key(), which the child-state session overrides instead of replacingkeyafter construction.StatePropertyis listed indjust.decorators.__all__.py/cyclic-importis excluded from CodeQL: every cycle it reported was guarded by a deferred import. The newtests/test_no_module_level_import_cycles.pyfails on any cycle made of module-level imports alone.
- A strict handler's parameter-validation error reaches the client from
Fixed
- A legacy component's view-level event alias now resolves only its own component type (#3078). Descriptor components (
Dropdown,Modal,Tabsand the others) register a view-level alias for theirMeta.event, such astoggle_dropdown. The alias looked up the client-suppliedcomponent_idwithgetattron the view, so an event for one component type could drive a component of another type, and could read any view attribute first. It now accepts only the view class's declared descriptors of its own type, and ignores any other id. The alias is also pinned to the legacy parameter policy, so a project-wide strictevent_parameter_policyno longer breaks it. - Worker-pool threads no longer keep their first caller's context alive (#3114). On Python 3.14+ a new thread starts with a copy of its starter's context (
sys.flags.thread_inherit_context, on by default in free-threaded builds). Theworker_threadspool started each thread lazily, on the first session's call, so that session's context values stayed referenced for the life of the process. The pool now starts its threads when it is created, in an empty context. - SSE and the HTTP fallback keep up with the page (found by ADR-034 C2's real-transport browser runs).
- SSE mount. The mount now morphs an HTTP-prerendered page against the mount HTML, as the WebSocket mount does (#1610). Before, it only stamped
dj-ids, so values that differ per mount stayed stale, including the identities of interactive components. Every event on such a component then failed with "Component not found". - HTTP zero-patch renders. A render that changed nothing no longer resets the server's diff baseline. That reset restarted the version at 1, the client's version check failed, and the page reloaded, losing its state. The answer is now an empty patch list with the new version.
- HTTP event ordering. Events are now sent one at a time, in order, like frames on a socket. Two in flight at once each restored and saved the session, so a form save could store stale values.
- SSE mount. The mount now morphs an HTTP-prerendered page against the mount HTML, as the WebSocket mount does (#1610). Before, it only stamped
- Strict pages kept working after a hot reload or
push_state()(staged ADR-036). The hot-reload patch frame andStreamingMixin.push_state()sent DOM updates without the public parameter-contract snapshot. That made a strict-policy client invalidate its contracts and refuse strict events until the next render, and a reload that changed handler declarations could advertise stale rules. Both now capture the snapshot in the same operation as the render; a hot reload whose contracts cannot be discovered falls back to a full page reload. Legacy sessions keep their frame shape. - The debug panel lists
@staticmethodevent handlers. Its handler list was a seconddir()/getattrwalk that missed them. It now shows exactly the handlers dispatch resolves (ADR-037). Pinned inpython/djust/tests/test_adr037_shared_discovery.py. dj-pastereaches the component or embedded child it is in. The paste binder never attached owner context, so a paste inside a LiveComponent or a{% live_render %}child was sent to the page's root view. It now attaches context like every other binding, under both parameter policies. Found by ADR-037's embedded-child browser test (tests/playwright/test_embedded_directives.py); a case intests/js/dj-paste.test.jspins it.- Cancelling an
assign_asyncloader now cancels it. The async runner caughtBaseException, socancel_async()or view teardown was swallowed: the task finished normally and the attribute became an erroredAsyncResultholding theCancelledError. The runners now catchException, so a loader's own failure is still surfaced, while cancellation (andKeyboardInterrupt/SystemExitin the sync runner) propagates and the attribute stays pending. Found by CodeQLpy/catch-base-exception. - A legacy page no longer loses every binding when parameter-contract discovery throws. Over HTTP, a discovery exception made the initial page publish
contracts: falseand a render return no update (a 500 on POST), even for a view that can only be legacy; the client then rejected every event. Now, as on the socket path, a view whose project policy is legacy and that declares no strict handler keeps its legacy page and response shape (python/djust/mixins/request.py). Where strict contracts can exist, or the client already advertised them, discovery failure still fails closed. - The HTTP fallback now reports failed events. When an event failed over the HTTP fallback, the client wrote only to the browser console, so an HTTP-only page could not show the failure. It now dispatches
djust:errorwith the server's error message, as the WebSocket and SSE transports do. The DEBUG error overlay and application listeners see it. - A queued HTTP-fallback event is no longer silently lost after an in-page
#anchorjump. HTTP events run one at a time, and a queued event checked that it still belonged to the page by comparing the full URL, fragment included (python/djust/static/djust/src/11-event-handler.js). The fragment never reaches the server, so it no longer counts. An event still dropped because the URL or root changed without a navigation now firesdjust:error; one dropped by real navigation stays quiet. - Legacy-policy apps no longer re-parse every rendered page that contains
dj-auto-recover. The ADR-036 R1 recovery downgrade only changes strict handlers, but the per-render recovery-target scan (python/djust/validation.py,note_rendered_recovery_targets) ran an HTML parse on every render and HTTP GET (about 3 ms for a 20 KB page, 29 ms for 200 KB), and policy resolution ran the class-level template scan for every handler. Both now run only when the project policy is strict, is invalid, or the view declares a strict handler. An invalid project policy still resolves a recovery target to legacy.
Security
- A legacy component's view-level event alias now resolves only its own component type (#3078). Descriptor components (
Dropdown,Modal,Tabsand the others) register a view-level alias for theirMeta.event, such astoggle_dropdown. The alias looked up the client-suppliedcomponent_idwithgetattron the view, so an event for one component type could drive a component of another type, and could read any view attribute first. It now accepts only the view class's declared descriptors of its own type, and ignores any other id. The alias is also pinned to the legacy parameter policy, so a project-wide strictevent_parameter_policyno longer breaks it. LiveViewTestClient.send_eventnow enforces handler authorization (#3094). It called the handler directly, so a test that sent an event to a@permission_requiredhandler as an unprivileged user passed whether the decorator was there or not. It now runs the consumer's gates first:@permission_required(denied when the view has no request), then the per-eventhas_object_permissionre-check for a view that overridesget_object, failing closed. A refused event does not run the handler and returnssuccess=Falsewithcode="permission_denied". A test that relied on the bypass must mount as a user who holds the permission.
Documentation
- Documented: interactive components (ADR-034, available from djust 1.3).
- A new "Interactive Components" guide covers the ownership rule, then
DropdownMenu: one instance, two menus, client-owned popovers with observations, delegated row actions versus keyed collections, and persistence, keyboard and security. All its examples are executed bypython/djust/tests/test_adr034_documented_examples.py. - The components API reference gains tables generated from the component's contracts by
scripts/generate-interactive-reference.py(make interactive-reference). A drift test and a pre-commit hook fail when they go stale. - The core-concepts page and the AI components reference point to the new guide.
- ADR-034 is accepted.
- A new "Interactive Components" guide covers the ownership rule, then
- Documented: editing one record with
ModelFormMixin, and ADR-035 is accepted. The form guide gains "Editing one record withModelFormMixin" and a migration recipe from_model_instance. The AI form reference gains the same pattern and its rules. Both examples are executed bypython/djust/tests/test_adr035_documented_examples.py. The existing_model_instancepattern still works and its examples are unchanged. - Typed event parameters are documented and ADR-036 is accepted. The events guide gains a "Typed event parameters (strict policy)" section: how to opt in, typed click and form examples, what the browser sends, the conversion rules, rejections, framework context and a migration checklist. The AI events reference leads with the strict form. Both sets of examples are executed by
python/djust/tests/test_adr036_documented_examples.pyandtests/js/adr036_documented_examples.test.js. The strict policy is a supported opt-in; legacy remains the default. - The Discord link in the docs works again.
discord.gg/djustreturned "Unknown Invite";CONTRIBUTING.md, the docs home page and the state-management guides now link to the permanent invitehttps://discord.gg/7sPKf3wtp9.
Removed
djust.V007("event handler missing**kwargs") is retired (ADR-037). A closed handler signature is now encouraged: a catch-all hides a misspelled parameter. djust no longer emits V007 and never reuses the ID. Existing V007 suppressions have no effect and can be removed.