djust 1.1.0rc3

Pre-releaseReleased

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

Added

  • Strict type enforcement on components/rust_handlers — the ADR-023 ratchet is COMPLETE (M4g, final module). This flips the LAST lenient holdout — the Rust template-engine component tag-registration shim (~193 inline/block render() handlers that parse untyped Rust-engine arg lists ["key=val", ...] into object-valued dicts and emit component HTML) — from the lenient mypy default to a strict island ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). This module was the sole sanctioned lenient exception (the genuinely-dynamic Rust-FFI boundary; an earlier attempt, M4b-1, found ~344 errors and documented it as intractable). It flipped clean with ZERO new # type: ignore (the only one in the file is the pre-existing _rust[import]). Two patterns did the work: (a) a typed module-level _safe() wrapper that casts Django's @keep_lazy-decorated (untyped → Any) mark_safe to str, absorbing the ~200-strong no-any-return cascade across every handler return without ignores; (b) inline cast(...) / str(...) (runtime no-ops) at each int()/float()/dict-key/attribute site of the kw.get(...) -> object cascade, plus a handful of explicit var: float/list[...]/dict[...] annotations. Render output is proven byte-identical — a deterministic-UUID parity harness rendered every handler against the pre-flip version and confirmed 382 outputs across all 193 handler classes are identical bytes (the cast/str coercions are runtime no-ops; the only str() wraps that touch lookup keys were converted to cast to guarantee key identity). mypy python/djust stays GREEN (822 files) with djust.components.rust_handlers strict; gate-off-verified (#1468) — a wrong-typed int return injected into a handler (ModalHandler.render, declared str) turns the gate RED ([return-value]), reverting restores GREEN. With M4g, no lenient exception remains in the components/ package — the global lenient default now parks only legacy non-components modules. Full suite 8604 passed / 0 failed. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the scaffolding/ + template_tags/ + theming/gallery/ subpackages — 20 modules (ADR-023 M4e, group 2). The next ratchet step flips three more subpackages from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans: the CRUD scaffolding generator (scaffolding/gen_live/gen_live_templates/generator/templates: the JSON/interactive schema-to-LiveView+admin code generator); the Rust-engine custom template-tag handlers (template_tags/{% url %}/{% static %}/{% djust_pwa %}/{% templatetag %}/{% dj_flash %}/{% djust_markdown %}/{% djust_client_config %}/{% live_render %} registered with the Rust renderer; this is the underscoretemplate_tags/ package, distinct from the Django-engine templatetags/ package already flipped in M4d group 1); and the theme-gallery / component-storybook surface (theming/gallery/views the gallery/editor/diff + storybook DEBUG/staff-gated views, context the example-context + token-serialization builders, component_registry, urls, storybook). None of the three subpackages has a tests/ dir, so the ratchet completes each in one PR with no test sub-package to defer. Annotated with real types (params + returns — not Any cosmetics): HttpRequest/HttpResponse on the gallery views, list[dict[str, Any]] on the example builders, Callable[[Type[TagHandler]], Type[TagHandler]] on the @register decorator factory. Render output is byte-identical — the SafeString/HTML boundaries (format_html in flash, escape in markdown, Template.render in pwa, reverse in url, static in static, _client_config_html in client_config, the dynamic component .render() in component_registry) return Any under the lenient global config (Django + the cross-island live_tags._client_config_html are seen as untyped), so each is coerced with str(...) at the boundary to satisfy warn_return_any WITHOUT changing the returned (already-safe) HTML. One real type fix: scaffolding/generator.pylist_display_fields annotated list[str] (was an un-annotated [] flagged var-annotated). mypy python/djust stays GREEN (822 files) with all 20 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typed int return injected into template_tags/url.UrlTagHandler.render (declared str) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the theming/themes/ theme-definition subpackage — 66 modules (ADR-023 M4f). The next ratchet step flips the per-theme definition subpackage from the lenient mypy default to a strict island via a single glob [[tool.mypy.overrides]] module = ["djust.theming.themes.*"] (ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any) — mirroring the djust.security.* glob pattern, so a new built-in theme file added to this directory is strict-by-default with no further pyproject edit. The subpackage is 63 per-theme data modules (default / nord / dracula / catppuccin / tokyo_night / gruvbox / … — each a flat set of module-level ColorScale/ThemeTokens/ThemePreset/DesignSystem/ThemePack literals, zero functions), the dependency-free re-export hub _base, the package __init__ (pure re-exports), and the deprecated _legacy module (the Theme/THEMES dataclass API kept for backward compat). 64 of the 66 modules were already strict-clean (data + re-exports), so M4f is mostly a config-flip; the only annotation work was on _legacy._DeprecatedThemesDict's nine untyped dict overrides (__getitem__/__contains__/get/items/keys/values/__iter__/__len__) — annotated to match the dict[str, Theme] superclass signatures (the three view methods declare -> Any for the un-nameable concrete dict_items/dict_keys/dict_values return types, the established codebase pattern). No real bugs found — the theme modules are pure data and _legacy's overrides were behaviorally correct, just unannotated (logic byte-identical; deprecation-warning behavior unchanged). mypy python/djust stays GREEN (822 files) with theming/themes/* strict; gate-off-verified (#1468) two ways — a wrong-typed str return on _legacy._DeprecatedThemesDict.__len__ (declared int) turns the gate RED ([override] + [return-value]), AND an untyped def injected into a theme-DATA module (nord.py) turns it RED ([no-untyped-def]), proving the glob covers the data modules and not just _legacy; reverting either restores GREEN. Behavior is byte-identical (annotations are runtime no-ops); full suite 8604 passed / 0 failed. Note: the top-level theming/ modules are already strict (M4c part 3), but mypy's djust.theming.* glob matches only direct children, not the deeper djust.theming.themes.X submodules, so this subpackage needed its own override entry. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the loose top-level modules + backends/ + db/ — 22 modules (ADR-023 M4e, group 1). The next ratchet step flips the independent loose top-level modules and the two leaf subpackages from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the loose top-level modules (apps DjustConfig, audit_ast AST security-audit walker, audit_live runtime auditor, bug_capture harness, checks_css_proposal proposed CSS system-checks, hooks lifecycle registry, hot_view_replacement HVR engine, state_backend/template_backend back-compat re-export shims, template_filters helpers, time_travel recorder, utils shared helpers + BackendRegistry, __main__ entry point) and the two leaf subpackages: the presence backends (base, memory, redis, registry, __init__) and the PostgreSQL LISTEN/NOTIFY bridge (decorators, exceptions, notifications, __init__). Annotated with real types (params + returns — not Any cosmetics): db/decorators.notify_on_save.decorate typed type[models.Model] so _meta/label resolve, with narrow # type: ignore[attr-defined]s on the dynamic _djust_notify_channel/_djust_notify_receivers introspection attrs stashed on/deleted from the decorated model class; the signal receivers _on_save/_on_delete annotated (sender: type, instance: Any, **_kw: Any) -> None. Four genuine clean-up fixes (the kind strict-flips surface, ADR-023, all behavior-preserving): backends.registry.get_presence_backend now cast(PresenceBackend, _registry.get()) mirroring the already-strict state_backends.registry pattern (the generic registry returns Any); backends.redis.RedisPresenceBackend.count wraps the untyped zcount Any-return in int(...); db.notifications._import_psycopg gained its -> tuple[Any, Any] return; and db.notifications._dsn_from_url's URL-field loop variable was renamed (valdsn_val) to stop colliding with the earlier str-typed parse_qsl loop var so the mixed str | int | None field tuple type-checks. mypy python/djust stays GREEN (822 files); gate-off-verified (#1468) — a wrong-typed int return in backends.registry.get_presence_backend (declared PresenceBackend) turns the gate RED ([return-value]), reverting restores GREEN. Behavior is byte-identical (annotations + the four wraps/rename are runtime no-ops); full suite 8604 passed / 0 failed. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the theming/templatetags/ + theming/management/ subpackages — 8 modules (ADR-023 M4e, group 3). The continuation of the theming ratchet: M4c (part 3) made the theming MACHINERY strict but explicitly deferred the user-facing render surface (the templatetag modules — theme_components was the heaviest at ~51 errors — plus the management command). This group finishes theming by flipping those deferred leaves from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the four template-tag modules (theme_components — the ~26 themed component tags theme_button/theme_card/theme_alert/theme_input/theme_modal/theme_table/theme_nav/etc.; theme_pages — the auth/error/utility page-fragment tags theme_login_page/theme_404_page/theme_maintenance_page/etc.; theme_tags — the theme_head/theme_css/theme_switcher/theme_preset/theme_mode accessors + the shared build_theme_head_context builder; theme_form_tagstheme_form/theme_form_errors/get_css_prefix) and the djust_theme management command (tailwind-config / export-colors / list-presets / shadcn-import-export / init / create-theme / validate-theme / create-package / check-compat / marketplace-info subcommands). These tags RENDER theme components into pages, so their mark_safe/format_html return values are annotated SafeString (the HTML-safe boundary) and context/request/form params get Context/HttpRequest | None/BaseForm; output is byte-identical. The management command uses the established CommandParser/*args: Any, **options: Any shape mirroring djust_setup_css/djust_doctor. Four real type fixes to clean the islands (the kind strict-flips surface, ADR-023): theme_components.theme_progress annotates percentage: float (the min(100, (int(value)/int(max))*100) reassignment is a float; the = 0 seed inferred int[assignment]); theme_tags.theme_framework_overrides narrows the format_html result through a str local at the unstubbed-django boundary ([no-any-return]); the three _css_prefix() helpers + theme_pages._csrf_token_value wrap the untyped get_theme_config().get(...)/get_token(...) boundary in str(...); and djust_theme.handle_marketplace_info reads the required-positional mp_theme_name via subscript (not .get()) so it stays non-Optional for the themes_dir / theme_name Path division + get_component_coverage(str, ...) call ([operator]/[arg-type]). mypy python/djust stays GREEN (822 files) with all 8 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typed int return in theme_pages._css_prefix (declared str) turns the gate RED ([return-value]), reverting restores GREEN. Behavior is byte-identical (annotations + the str(...) boundary coercions are runtime no-ops) apart from the four genuine fixes above; full suite 8604 passed / 0 failed (1878 theming tests green). This completes theming/ except the optional theming/gallery subpackage, which remains for a continuation batch. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the pwa/ + optimization/ + tenants/ + observability/ subpackages — 31 modules (ADR-023 M4d, part 2). The next ratchet step after M4c (theming/ + admin_ext/) flips every non-test module of these four optional-extra subpackages from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the PWA layer (mixins PWAMixin/OfflineMixin/SyncMixin, storage offline backends + OfflineAction/SyncQueue, sync SyncManager/ConflictResolver, manifest, service_worker, utils), the optimization layer (fingerprint StateFingerprint/SectionCache/IncrementalStateSync, codegen serializer code-gen, query_optimizer select/prefetch analysis, cache SerializerCache, __init__), the multi-tenant layer (resolvers, managers TenantManager/TenantQuerySet, backends redis/memory presence, middleware ContextVar tenant binding, mixin TenantMixin/TenantScopedMixin, audit, security, models, __init__ — annotations only; tenant-isolation logic byte-identical), and the observability layer (views localhost-gated endpoints, middleware localhost gate, sql/timings/log_handler/tracebacks capture buffers, dry_run side-effect blocker, registry, urls, __init__). Annotated with real types (params + returns — not Any cosmetics), using the established mixin-collaborator pattern (# type: ignore[misc] on cooperative super().get_context_data()/dispatch() calls mirroring wizard.py/tenants; TYPE_CHECKING-only push_event/sync_queue stubs on the PWA mixins documenting the co-mixed-LiveView contract) and a narrow # type: ignore[import-untyped] on dry_run's lazy import requests (a known-stub package mypy won't silence via ignore_missing_imports). Two real bugs fixed to clean the islands (the kind strict-flips surface, ADR-023): pwa.storage.OfflineAction.id widened to Union[str, int] (callers forward an int model pk as obj_id; the SyncQueue action-id params widened to match), and pwa.mixins.delete_offline now passes the required OfflineAction(data={}) — omitting it raised TypeError at runtime on every call (a guaranteed crash in an untested path). mypy python/djust stays GREEN (822 files) with all 31 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typed str return in optimization.fingerprint.StateFingerprint.version (declared int) turns the gate RED ([return-value]), reverting restores GREEN. Behavior is byte-identical (annotations are runtime no-ops) apart from the two genuine bug fixes above; full suite 8604 passed / 0 failed. Remaining for a continuation batch: the pwa/{templatetags,management}-style leaf packages do not exist for these four subpackages, so M4d(2) completes their non-test surface. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the tutorials/, api/, template/, and state_backends/ subpackages — 20 modules (ADR-023 M4d, part 3). The next ratchet step flips four independent transport/render/persistence subpackages from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the declarative guided-tour state machine (tutorials/ — the TutorialStep dataclass + TutorialMixin async tour loop, with if TYPE_CHECKING: declarations for the sibling-mixin surface it cooperates with — push_commands/_flush_pending_push_events/wait_for_event), the opt-in HTTP-API transport (api/ — the @event_handler(expose_api=True) + @server_function dispatch views, the pluggable BaseAuth/SessionAuth contract, the view registry, the OpenAPI schema builder, and the URL wiring), the Rust template engine's Django backend (template/DjustTemplateBackend.get_template/from_string, the multi-line {# #}get_contents loaders, the DjustTemplate rendering pipeline incl. the {% extends %}/{% block %} parser + {% url %} resolver, and the serialize_valueJSONValue serializer), and the LiveView state-persistence backends (state_backends/ — the StateBackend ABC, the in-memory + Redis backends, and the registry). api/ and state_backends/ are security/correctness-relevant — annotations only, logic byte-identical: the _snapshot_assigns/_compute_changed_keys diff, the CSRF/auth/object-perm gates, the rate-limit checks, the msgpack round-trip + identity-guarded cache pop, and the zstd compression path are UNTOUCHED. Three PyO3 methods consumed by the state backends (RustLiveView.serialize_msgpack / deserialize_msgpack / get_timestamp) were added to the _rust.pyi wire-boundary stub (they existed at runtime but were missing from the stub). The only narrow coded # type: ignores are at genuine dynamic edges (the optional-JIT DjangoJSONEncoder = None / _get_model_hash = None import fallbacks in template/rendering.py; the transient None-view health-check probe entry in state_backends/memory.py); cast(...) is used at the Django/zstd/Rust unstubbed-boundary Any leaks, and assert ... is not None narrows already-guarded optionals (the next_start.end() block-parser sites, the _get_compressor() compress path gated by _compression_enabled). mypy python/djust stays GREEN (822 files) with all 20 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typed int return in state_backends/registry.get_backend ([return-value]) turns the gate RED, reverting restores GREEN. Full suite 8604 passed / 0 failed. None of the four subpackages has a tests/ subdir, so the ratchet completes each in a single PR (no test sub-package to defer). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the management/, checks/, auth/, and templatetags/ subpackages + 8 loose top-level modules — 53 modules (ADR-023 M4d, group 1). The ratchet step after the M4c subpackages (mixins/ + admin_ext/ + theming/) flips four more subpackages and the independent loose modules from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans every management command (djust_audit, djust_check, djust_doctor, djust_setup_css, djust_typecheck, djust_gen_live, djust_new, djust_schema, djust_mcp, djust_ai_context, generate_sw, cleanup_liveview_sessions) + the shared _introspect helper; the full Django system-check family (configuration/security/templates/quality/components/integrations/accessibility + the shared utils); the auth layer (the check_view_auth/run_pre_mount_auth/enforce_object_permission security core, the LoginRequiredLiveViewMixin/PermissionRequiredLiveViewMixin, the social_auth_providers context processor, the signup/login views + forms, and the djust_admin plugin + its OAuthProvidersView/SocialAccountsView LiveView pages); all five template-tag modules (live_tags — the big one with {% live_render %}/{% colocated_hook %}/{% dj_activity %} + the lazy-thunk emitter, plus djust_flash/djust_formsets/djust_pwa/djust_tutorials); and the loose modulescli, dev_server, deploy_cli, drafts, http_streaming, session_utils, push, middleware. Annotated with real types (params + returns — not Any cosmetics): SafeString at the mark_safe/format_html boundary; CheckMessage for system-check errors lists + returns; argparse.Namespace/CommandParser for the management commands; ast.* node types (ast.ClassDef/ast.Call/ast.expr/ast.Module) for the AST-based checks; AsyncIterator[bytes] for the ChunkEmitter streaming surface. The only narrow coded # type: ignores are at genuine dynamic edges: the djust.checkssetattr re-export (_root.* — the patch-by-path contract from the #1822 monolith split), the djust-admin optional-dependency fallback class (no-redef/assignment), the optional _rustversion export (not in the .pyi), the auth-mixin cooperative super().dispatch (provided by the combined View), and the Django model._meta access (no django-stubs). checks/ + auth/ logic is byte-identical — annotations + cast(...)/bool(...) boundary coercions are runtime no-ops; the system-check AST walkers, suppression logic, and the auth precedence (login → permission → custom hook → Django AccessMixin → object-permission) are UNTOUCHED. mypy python/djust stays GREEN (822 files); gate-off-verified (#1468) — a wrong-typed return in a flipped module (templatetags/djust_flash.dj_flashint) turns the gate RED ([no-any-return]), reverting restores GREEN. Full suite 8604 passed / 0 failed. requests (consumed by deploy_cli) joins yaml in the untyped-third-party override. Remaining for a continuation batch: the management/templatetags-adjacent long tail is already covered; the tenants/, backends/, and state_backends/ subpackages + the last few loose modules remain. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the theming/ subpackage — 39 top-level modules (ADR-023 M4c, part 3). The next ratchet step after the components/ batches (M4b) flips every top-level module of the theming system — including its small rust_handlers (unlike the components/ one, this one was already well-typed and is NOT the iceberg) — from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the registry layer (_registry_accessor singleton + registry discovery wiring), the ThemeManager + ThemeState state/session machinery, the CSS generators (theme_css_generator, pack_css_generator, component_css_generator, design_system_css, css_generator), the color machinery (palette, colors, accessibility, high_contrast, presets, design_tokens), the render paths (context_processors, template_resolver, mixins ThemeMixin, components, forms renderer), the build/adapters/tooling (build_themes, shadcn, tailwind, inspector, checks, manifest, loaders, theme_packs, compat, contracts), the apps AppConfig, views, urls, and the leaf _config/_constants/_types/_builtin_presets modules. Annotated with real types (params + returns — not Any cosmetics), using the cast(str, mark_safe(html)) boundary pattern for the theme-component renderers (django's safestring is unstubbed, so mark_safe returns Any; SafeString itself resolves to Any without django-stubs, so a str cast is the honest no-Any-leak shape). mypy python/djust stays GREEN (822 files) with all 39 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typed int return in manager.get_css_prefix ([return-value]) turns the gate RED, reverting restores GREEN. Rendering byte-identical — annotations + cast(...) + int(hue_offset) casts are runtime no-ops; the only behavior-adjacent additions are defensive if self._theme_manager is None: return guards in the four ThemeMixin event handlers (no-ops on the real post-mount path, matching the existing _setup_theme_context guard). Full suite 8604 passed / 0 failed; 1863 theming tests pass (incl. the previously-flaky test_theme_tags_rust_engine_1721, green via #1929's fixture). Remaining theming/ for a continuation batch: the theming/{templatetags,management,gallery} subpackages (the templatetag modules are the heaviest — theme_components ~51 errors — so they're a separate batch). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the mcp/ + contrib/uploads/ + uploads/ subpackages — 14 modules (ADR-023 M4d, group 4). The next ratchet step flips three independent subpackages from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans: the MCP server (mcp/server, mcp/__init__, mcp/__main__ — the AI-assistant introspection/scaffolding tool: create_server() -> "FastMCP" via a TYPE_CHECKING-guarded import so the optional mcp dep is never imported at module load, _ensure_django() -> bool, main() -> None, and the observability-tool returns); the binary-WebSocket-frame upload system (uploads/__init__ — the UploadWriter base + BufferedUploadWriter + UploadConfig + UploadManager, uploads/resumable — the resumable chunk protocol, uploads/storage — the in-memory + Redis UploadStateStore impls, uploads/views — the UploadStatusView HTTP endpoint with HttpRequest/JsonResponse annotations); and the contrib upload-writer adapters (contrib/__init__, contrib/uploads/{__init__,azure,errors,gcs,s3_events,s3_presigned} — the S3 presigned/event, GCS resumable, and Azure block-blob direct-to-storage writers). None of these has a tests/ dir, so the ratchet completes in one PR with no test sub-package to defer. Annotated with real types (params + returns — not Any cosmetics); the only narrow coded edges are: # type: ignore[override] on the legacy write_chunk(self, chunk) adapters (BufferedUploadWriter, GCSMultipartWriter, AzureBlockBlobWriter) — the dropped trailing chunk_index default is an INTENTIONAL, runtime-dispatched part of the UploadWriter contract (_writer_accepts_chunk_index introspects the signature; documented on the base method), and cast(...) narrows at the untyped boundaries (json.loads in uploads/storage, boto3.generate_presigned_url in s3_presigned, requests.Response.text + session.session_key in mcp/server/uploads/views). Two mcp/server observability-tool params dicts inferred homogeneous-then-mutated-with-the-other-type were annotated dict[str, object]. Third-party requests (consumed by mcp/server + contrib/uploads/gcs, no stubs) is marked untyped in the shared ["yaml", "requests"] override. Upload logic is byte-identical — these are security-relevant binary-frame handlers; annotations + cast(...) are runtime no-ops and NO chunk-dispatch, size-cap, or HMAC logic was altered. mypy python/djust stays GREEN (822 files); gate-off-verified (#1468) — a wrong-typed return injected into uploads/storage.delete turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed (413 upload/mcp-related tests pass). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the admin_ext/ subpackage — 13 modules (ADR-023 M4c, part 2). The next ratchet step after the components/ batches (M1 foundation → M2 public-API quartet → M3 dispatch core → M4a loose top-level → M4b-1/2/3 components/) flips the entire Django-admin integration from the lenient mypy default to a strict island ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans every non-test admin_ext/ module: the DjustAdminSite (model + plugin registration, URL generation, app-list / plugin-nav / widget collection), DjustModelAdmin (the list/detail/form/action config + queryset auto-optimization), the plugin system (AdminPlugin / AdminPage / AdminWidget / NavItem), the LiveView-based admin views (AdminIndexView, ModelListView, ModelDetailView, ModelCreateView, ModelDeleteView, LoginView, LogoutView + the admin_login_required wrapper and the _VIEW_REGISTRY plumbing), the AdminFormMixin (FK/M2M option loading, date/time field detection, readonly handling, real-time field validation), the bulk-action progress widget + @admin_action_with_progress decorator, the AdminTailwindAdapter admin CSS-framework adapter, the register/action/display decorators, the DjustAdminConfig AppConfig, the autodiscover package __init__, and the admin template-tag helpers (get_item/get_field/concat/admin_url). Excludes admin_ext/tests/, which stays on the lenient global default. Annotated with real types (params + returns — not Any cosmetics): HttpRequest/Optional[models.Model] on request/obj params, typed class-attr config (list_filter: List[Any], formfield_overrides: Dict[Any, Any], widget_id: Optional[str], …), List[URLPattern] URL builders, and the established mixin-collaborator pattern (request: Any / _model: Any / _model_admin: Any annotation-only attrs on AdminBaseMixin + AdminFormMixin documenting the co-mixed-LiveView contract, plus a # type: ignore[misc] on the cooperative super().as_view() mirroring wizard.py); decorator function-attribute stamping (wrapper.short_description = ...) carries narrow # type: ignore[attr-defined] at the genuine dynamic edge. mypy python/djust stays GREEN (822 files) with all 13 strict and the rest lenient; gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict module (adapters.register_admin_adaptersint) turns the gate RED ([return]), reverting restores GREEN. Behavior is byte-identical — annotations are runtime no-ops; the 95 admin tests (test_admin_basic/test_admin_plugins/test_admin_widgets_per_page/test_bulk_progress + admin checks) and the full suite (8604 passed / 0 failed) confirm no regression. See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the mixins/ subpackage — 21 modules (ADR-023 M4c, part 1). The eighth ratchet step (after M1 foundation, M2 public-API quartet, M3 dispatch core, M4a loose top-level, M4b-1/2/3 all of components/) flips the entire mixins/ subpackage — the LiveView mixin layer that composes the public LiveView class — from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans all 21 modules: the small leaf mixins (flash, layout, page_metadata, post_processing, async_workstart_async/defer/assign_async, waiterswait_for_event, model_binding — the dj-model mass-assignment guard, components — the child-component lifecycle), the already-clean leaves (__init__, activity, handlers, navigation, notifications, push_events, sticky, streams), the context/JIT serialization mixins (contextget_context_data/_apply_context_processors/_deep_serialize_dict, jit_jit_serialize_queryset/_jit_serialize_model/_get_template_content), the HTTP request mixin (get/aget/post + the streaming _make_streaming_response/_is_asgi_context), the Rust-bridge / change-detection mixin (rust_bridge_sync_state_to_rust/_initialize_rust_view/_normalize_db_values), and the large template rendering mixin (render/render_full_template/render_with_diff/arender_chunks + the HTML extraction/stripping helpers). Annotated with real types (params + returns — not Any cosmetics), using the established if TYPE_CHECKING: host-attribute-declaration pattern (mirroring streaming.py) for the cross-mixin/host-class surface each mixin cooperates with (get_context_data, _rust_view, template_name, etc.) — a runtime no-op resolved only at type-check time, since a mixin is never instantiated standalone. The only narrow coded # type: ignores are at genuine dynamic edges (the optional-Rust RustLiveView = None / extract_template_variables = None import fallbacks; the event_handler direct-file-import fallback shim; the dynamic component_id/_auto_id attribute sets on the Component | LiveComponent union). rust_bridge/jit change-detection is byte-identical — annotations are runtime no-ops; the _sync_state_to_rust change-detection, the _framework_attrs-class filter conventions, and all id()/value comparison logic are UNTOUCHED (no comparison or filter expression was altered). mypy python/djust stays GREEN (822 files); gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict mixin (template.get_templateint) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. The mixins/ ratchet completes in a single PR (no mixins/tests/ sub-package exists to defer). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the FINAL components/ modules — 68 modules (ADR-023 M4b, part 3). The seventh and last components/ ratchet step (after M1 foundation, M2 public-API quartet, M3 dispatch core, M4a loose top-level, M4b-1 core machinery, M4b-2 UI catalog) flips every remaining components/ module — except the deliberately-lenient rust_handlers — from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the component template-tag layer (templatetags/djust_components ~373 fns, _advanced ~84, _forms ~26, _charts ~23 — every Node.render(self, args/content, context) -> SafeString, do_*(parser, token) -> template.Node, and @register.simple_tag/inclusion_tag function, with inclusion_tags correctly typed -> dict[str, Any] since they return a context dict, not HTML), the per-widget mixins/data_table (the DataTableMixin — its ~21 on_table_* event handlers, handle_* override hooks, get_*/_apply_* pipeline, and the safe-arithmetic expression parser), the gallery LiveView surface (gallery/live_viewsGalleryCategoryMixin + 9 category views, with the template_name Liskov conflict resolved by a TYPE_CHECKING-only LiveView base alias; views, examples, registry, context_processors, and the component_gallery management command), the ~24 remaining components/components/* widgets with untyped private-helper params (_render_node/_squarify/_compute_diff/_eval_expression/etc.), the layout/tabs/data/pagination/ttyd/terminal leaves, and the ui/*_simple stateless widgets + ui/dropdown (the over-narrow nav-item dict widened to the honest Any contract per #1108; the optional-Rust import shims — from djust._rust import RustX / RustX = None fallbacks for built-but-unstubbed and declared-but-unbuilt Rust component classes — carry narrow # type: ignore[attr-defined]/[assignment, misc] at the genuine dynamic edge). Annotated with real types (params + returns), using the mark_safe(...) -> SafeString boundary pattern (no Any leak). rust_handlers is deliberately left LENIENT — it is a genuinely-dynamic Rust-bridge registry whose 193 handlers parse untyped Rust-engine arg lists into dict[str, object] (the kw.get() -> object cascade), so strict typing surfaces 344 errors (203 no-any-return + 91 call-overload + …) that would need >200 narrowing changes / # type: ignores with real rendering-behavior risk; the global lenient default is the correct home for it (documented exception in pyproject + this entry). mypy python/djust stays GREEN (823 files) with all 68 strict and rust_handlers lenient; gate-off-verified (#1468) — a wrong-typed return in mixins/data_table ([return-value]) and a dropped annotation in templatetags/djust_components ([no-untyped-def]) each turn the gate RED, reverting restores GREEN. Rendering byte-identical — annotations are runtime no-ops, verified by diffing the rendered HTML of all 7 chart tags, 8 representative widgets (diff_viewer/prompt_editor/heatmap/treemap/json_viewer/org_chart/pivot_table/animated_number), and 8 djust_components simple_tags against the pre-change versions (identical output), plus 143 component/data_table tests passing. Full suite 8604 passed / 0 failed. This completes the ADR-023 components/ ratchet (only the documented rust_handlers exception remains lenient within components/). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the components/ UI catalog + templatetag helpers — 185 modules (ADR-023 M4b, part 2). The sixth ratchet step (after M1 foundation, M2 public-API quartet, M3 dispatch core, M4a loose top-level batch, M4b-1 core component machinery) flips the component UI catalog and small leaf modules from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any). Spans the full components/components/ widget catalog (146 modules — alert, badge, card, spinner, kanban-adjacent leaves, charts, etc.), the ui/ stateless widgets (8 — spinner, modal, alert, progress, badge, button, card, list_group), the data//forms//layout//gallery//management//ttyd/ leaf packages, the descriptor-based components (descriptors/* — the DEP-002 Accordion/Tabs/Modal/Sheet/Dropdown/Collapsible/Carousel/Tooltip + base), and the 8 deprecated state mixins (mixins/tooltip, tabs, sheet, modal, dropdown, collapsible, carousel, accordion). Annotated with real types (params + returns — not Any cosmetics): typed *_instances class vars (Optional[Dict[str, XState]]), instance_id: str / component_id: str / is_open: bool params, get_*_ctx(...) -> Dict[str, Any] accessors, and render() -> SafeString (mirroring the markdown.py island — mark_safe(...) returns Any under django's unstubbed safestring, so SafeString is the correct str-compatible annotation that cleanly absorbs the Any without a # type: ignore). Rendering is byte-identical — annotations are runtime no-ops, verified by diffing the rendered HTML of representative UI components (spinner/alert/modal) against the pre-change versions (identical output). mypy python/djust stays GREEN (822 files) with all 185 strict and the rest lenient; gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict module (ui/spinner, descriptors/modal) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. Remaining components/ for the continuation batch: the Rust-bridge handler registry (rust_handlers — the ~360-error mark_safe/kw.get()-object iceberg, a separate decision), the per-widget mixins/data_table, the big templatetag modules (templatetags/djust_components/_advanced/_forms/_charts), the gallery/live_views/views/examples LiveViews, the ~24 components/components/* widgets with untyped private-helper params (_render_node/_squarify/_compute_diff/etc.), and the union-typed ui/*_simple widgets + ui/navbar_simple/modal/dropdown (over-narrow dict inference + the declared-but-unbuilt RustNavBar/Rust* fallback imports). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the components/ machinery — 15 core modules (ADR-023 M4b, part 1). The fifth ratchet step (after M1's foundation, M2's public-API quartet, M3's dispatch core, M4a's loose top-level batch) flips the core component-system machinery — NOT the UI catalog — from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any): components.__init__, components.apps, components.registry (the LiveComponent name registry), components.assigns, components.dependencies (the DependencyManager CSS/JS asset registry), components.function_component (the @component decorator + {% call %}/{% slot %} dispatch handlers), components.helpers, components.presets (the tag-preset registry), components.icons (the Heroicons SVG renderer + render_icon), components.suspense (the {% dj_suspense %} fallback renderer), components.server_event_toast (ServerEventToastMixin), components.utils (shared format_cell/interpolate_color/interpolate_color_gradient + CURRENCY_SYMBOLS), components.mixins.base (the per-component interactive mixin base — ComponentMixin + the TypedState dict subclass), components.templatetags._registry (the shared template.Library + the security-sensitive safe_url scheme-validator + _resolve/_parse_kv_args), and components.templatetags._dev_tools (the Terminal/MarkdownEditor/JsonViewer/LogViewer/FileTree dev-tool template tags). Annotated with real types (params + returns — not Any cosmetics); the only narrow coded # type: ignore[attr-defined] are at genuine dynamic edges (the @component decorator stamping _djust_* metadata onto a plain Callable; the per-invocation _slots/_children attached to a LiveComponent instance for template render). The mark_safe-returns-Any boundary is handled with typed-local narrowing (a small _safe(html: str) -> str wrapper in _dev_tools, str-typed locals elsewhere) — no Any leak. mypy python/djust stays GREEN (822 files) with all 15 strict and the rest lenient; gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict module (utils.interpolate_color) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. Remaining components/ for the continuation batch: the Rust-bridge handler registry (rust_handlers, ~360 errors once -> str is added — the mark_safe/kw.get()-object iceberg), the per-widget mixins.data_table, and the big templatetag modules (djust_components/_advanced/_forms/_charts), plus the UI catalog (ui/, data/, forms/, gallery/, charts UI). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on 15 loose top-level modules (ADR-023 M4a). The fourth ratchet step (after M1's foundation, M2's public-API quartet, M3's dispatch core) flips a batch of independent, low-cross-risk top-level modules from the lenient mypy default to strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any): serialization (the wire-boundary JSON/normalizer — DjangoJSONEncoder._serialize_model_safely + the finding-#19 denylist/allowlist/opt-out field gate + normalize_django_value), config, __init__, routing (the live_session URLconf walk + auth-filtered route-map emit), formsets, simple_live_view, testing (the public LiveViewTestClient + SnapshotTestMixin + LiveViewSmokeTest fuzz/smoke harness), react, rust_components, frameworks (the CSS framework adapters), js (the JS command-chain builder), wizard (WizardMixin), performance, profiler, and presence (PresenceMixin + LiveCursorMixin). Annotated with real types (params + returns — not Any cosmetics); the only # type: ignore[misc] are at genuine mixin super()-delegation edges (wizardmount/get_context_data, which the LiveView MRO supplies at runtime). mypy python/djust stays GREEN (822 files) with all 15 strict and the rest lenient; gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict module turns the gate RED, reverting restores GREEN. Full suite 8604 passed / 0 failed. The ratchet continues one batch per PR (the remaining long tail: mixins/components/theming/CLI). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the dispatch/runtime core — runtime, websocket, sse, streaming, websocket_utils (ADR-023 M3). The five modules that form the WebSocket/SSE/ViewRuntime dispatch spine (every mount + event flows through them) are now mypy strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any), the third ratchet step after M1's foundation and M2's public-API quartet. Safe to type now that the ADR-022 ViewRuntime convergence has settled (no spine code about to move). Annotated with real types (params + returns — not Any cosmetics): runtime.py (42 strict errors — ViewRuntime dispatch helpers, _build_request/_check_auth/_extract_*, the actor-mount path, _tenant_context), websocket.py (73 — LiveViewConsumer lifecycle connect/disconnect/receive, the handle_* verb handlers, the Channels event handlers server_push/db_notify/presence_event/etc., _run_async_work/_dispatch_single_event, _mount_one's 5-tuple return, the module helpers _snapshot_assigns/_compute_changed_keys/render_embedded_child_html), sse.py (17 — the DjustSSE*Viewget/post HTTP handlers, the owner-binding helpers, the SSE event-stream async generator), streaming.py (6 — StreamingMixin, with TYPE_CHECKING host-class attribute declarations), and websocket_utils.py (7 — the shared event-security pipeline). Only two narrow coded # type: ignore[arg-type] for genuine frame-dynamic edges (the dormant actor-event-name forward; the no-binary receive() text frame). mypy python/djust stays GREEN (822 files); gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict module turns the gate RED, reverting restores GREEN. Full suite 8604 passed / 0 failed. The ratchet continues one module per PR (M4: mixins/components/theming/long tail). See docs/adr/023-incremental-type-enforcement.md.

  • Strict type enforcement on the public-API quartet — live_view, component, decorators, forms (ADR-023 M2). The four developer-facing modules that py.typed exposes to downstream consumers are now mypy strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any), the second ratchet step after M1's foundation. Annotated: live_view.py (as_view/__init__/live_view decorator + private-state helpers) and its PEP 561 stub live_view.pyi; components/base.py (the Component + LiveComponent public bases — descriptor protocol, render waterfall, event-handler factory); decorators.py (@event_handler, @action, @server_function, @reactive, @state, @computed, @optimistic, @background + their nested wrappers/descriptors); and forms.py (FormMixin + LiveViewForm). mypy python/djust stays GREEN (822 files); the strict flip is gate-off-verified (#1468) — injecting a wrong-typed return into one of the four turns the gate RED, the same error in a lenient module stays GREEN. The ratchet continues one module per PR (M3: the dispatch/runtime core). See docs/adr/023-incremental-type-enforcement.md.

  • Enforced incremental type-checking — a mypy merge gate + strict islands + the _rust.pyi boundary (ADR-023). djust ships py.typed (PEP 561 — downstream consumers type-check against djust's hints), and pyproject.toml declared a strict [tool.mypy] config — but mypy was invoked nowhere (CI / Makefile / pre-commit), so the strict config was dead and mypy python/djust reported 8,421 errors (≈6,814 missing annotations + ~750 missing-stub imports + ~700 real type errors). This PR restructures [tool.mypy] for incremental adoption: a lenient global default (ignore_missing_imports = true + ignore_errors = true) that parks the legacy baseline so the gate is GREEN, plus per-module strict islands ([[tool.mypy.overrides]] with ignore_errors = false + disallow_untyped_defs + disallow_incomplete_defs + warn_return_any) that genuinely enforce every error class on a 22-module starter set led by the security boundary (djust.security.*) and the PyO3 wire boundary (djust._rust, typed by _rust.pyi), plus rate_limit, validation, permissions, markdown, schema, signals, async_result, test_isolation, and the well-annotated _-prefixed leaf modules (_client_ip, _log_utils, _html, _view_resolution, _deprecation, _context_provider). The gate is enforced: a non-continue-on-errormypy step in the python-tests CI job (a new MERGE GATE — #1236 governance — wired into the test-summary AND-condition; ships gating because it is green by construction, per #1534), a make typecheck target (in make check), and a scoped pre-commit hook on python/djust/**.py{,i} changes. The _rust.pyi stub's top-level names are pinned to exactly match the compiled module's runtime exports and a strict island (markdown) imports through it, so the wire/serialization boundary is type-checked, not merely declared. Empirical canary (#1459): an injected missing-annotation / wrong-typed-return in a strict island makes the gate RED, while the same error in a lenient module stays GREEN — the gate is real, not cosmetic. The ratchet is one-module-per-PR, prioritising the developer-facing public API (live_view/component/decorators/forms) since py.typed exposes it. See docs/adr/023-incremental-type-enforcement.md.

Changed

  • CI: CodeQL config excludes py/ineffectual-statement (false-positive noise from the ADR-023 TYPE_CHECKING stub idiom). The strict-mypy ratchet added if TYPE_CHECKING: forward-declaration blocks across the mixins (cooperating-attribute/method stubs with ... bodies so each strict-island mixin resolves names supplied by sibling classes at MRO time — zero runtime effect). CodeQL's py/ineffectual-statement flags every ... Ellipsis body; all 50 hits were this idiom (the genuine useless-expression class is covered by ruff). Added the rule to .github/codeql/codeql-config.yml's query-filters so it doesn't recur as the type-checking blocks grow. Also converted a cast("Any", …) string forward-ref to a direct cast(Any, …) in components/rust_handlers.py so CodeQL sees the Any import as used (#2553).

  • CI: the shared Playwright harness now waits for the demo server's actual canary route to be ready before the browser run — removes the cold-cache page.goto flake on the BLOCKING browser-smoke gate (#1943). The blocking browser-smoke gate (#1869) intermittently red-barred unrelated PRs with Page.goto: Timeout 30000ms exceeded navigating to /demos/browser-smoke/: on a cold-cache run that compiles the djust_components Rust crate from scratch, the demo uvicorn server wasn't ready within the fixed 30s page.goto deadline (it cleared on a warm-cache re-run — not a real break). The .github/actions/djust-playwright-server readiness step (shared by every playwright job) is fixed at the source: (1) its poll bound is bumped 30s → 120s (60 attempts × 2s); (2) it now polls the ACTUAL canary target route (/demos/browser-smoke/) in addition to /, using curl -fsS so a half-initialized app's 5xx does NOT count as ready — this warms the route (first-hit lazy import/compilation) DURING the bounded loop so page.goto lands on an already-warm route instead of racing its own deadline. tests/playwright/test_browser_smoke.py's page.goto also gets an explicit 60s timeout (belt-and-suspenders, up from the 30s default). The gate's SIGNAL is preserved: a genuinely-down server still fails LOUD (exit 1 + server.log dump) once the 120s bound is hit, so a real runtime break of either #1849/#1848 class still red-bars the PR.

Fixed

  • HTML preserve-block regexes now match end tags with trailing whitespace (</script >, </style\n>) — CodeQL py/bad-tag-filter#2482._strip_comments_and_whitespace() (mixins/template.py) masks <script>/<style>/<pre>/<code>/<textarea> raw-text blocks behind placeholders before the HTML-comment-strip + whitespace-collapse passes, so their bodies aren't corrupted. The end-tag patterns used a bare </tag>, but per the HTML5 tokenizer an end tag closes on </tag followed by whitespace, /, bogus attributes, or > — so </script >, </script\n>, and even </script bar> all close a <script> in a browser. The bare pattern missed those forms, so the block was NOT preserved, letting a comment-looking token inside the JS/CSS body (var s = '<!-- x -->') get stripped and the script corrupted. All five patterns now use </tag[^>]*> (CodeQL's recommended form, matching every close variant). New TestEndTagWhitespacePreservation (whitespace + newline + bogus-attribute cases, gate-off verified) in test_strip_whitespace.py.

  • _run_async_work no longer writes against a stale view on disconnect/re-mount mid-await (#1940).LiveViewConsumer._run_async_work runs as a detached ensure_future task that captures view = self.view_instance before its first await (the background callback). If a disconnect (which nulls view_instance) or a live_redirect / re-mount (which reassigns view_instance to a NEW view) interleaved during that await window, the completed task ran its handle_async_result + _sync_state_to_rust + render_with_diff + source="async" frame against the torn-down or replaced view — a pre-existing untested race (#245/#1198 TOCTOU class). Added an identity-guard after the callback await on both the success and error paths: if the consumer's live view is no longer the captured one, the stale re-render is dropped. Cancellation can't stop the in-flight worker thread (sync_to_async runs in a thread pool), so an identity-guard — not task cancellation — is the correct cure. The normal (no-teardown) async-work path is byte-identical. New cases in TestRunAsyncWorkTeardown (python/djust/tests/test_run_async_work_teardown_1940.py), gate-off verified.

  • TutorialMixin now initializes its four internal tutorial-signal attrs in __init__, not the tutorial_total_steps setter (#1952)._tutorial_active_target, _tutorial_active_class, _tutorial_skip_signal, and _tutorial_cancel_signal were previously initialized inside the tutorial_total_steps SETTER, so a TutorialMixin view that never set tutorial_total_steps (or read those attrs before the setter ran) hit AttributeError — e.g. _cleanup_active_step() (called from start_tutorial's finally block) reads _tutorial_active_target/_class, and skip_tutorial/cancel_tutorial read the skip/cancel signals once running. The four attrs now default to None in __init__ (placed alongside the existing _tutorial_running/_tutorial_current_step/_tutorial_total_steps inits); the setter keeps its sole job of updating _tutorial_total_steps. Surfaced by ADR-023 M4d typing (PR #1951), left untouched then per #1079 typing-PR scope. New regression cases in TestSignalAttrsInitializedInInit (read the four signals without invoking the setter; pre-fix raised AttributeError).

  • ComponentMixin.update_component no longer raises AttributeError on a LiveComponent (#1947).update_component(component_id, **props) calls component.update(**props), but LiveComponent (a subclass of ContextProviderMixin, NOT Component) had no update() method at runtime, so any LiveComponent that did not define its own update() raised AttributeError on that path (the latent bug annotated with a # type: ignore[attr-defined] in ADR-023 M4c, part 1). LiveComponent now has a base update(**kwargs) that sets each prop as an instance attribute (mirroring the Python/hybrid path of Component.update) and returns self for chaining — the same component.update(**props) API the component docs already document. Subclass update() overrides still take precedence. The stale # type: ignore[attr-defined] at the call site is removed. Regression coverage in TestUpdateComponentNoUpdateOverride (tests/unit/test_component_parent_communication.py): bare-LiveComponent update through update_component, base-update() chaining returns self, subclass-override-still-wins.

  • Dev-env: detect + recover the core.bare = true shared-config corruption that breaks worktree + main checkout (#1938). A linked git worktree shares one .git/config with the main checkout; if anything flips core.bare to true there (a build/PyO3-repoint step running git config core.bare true, an IDE/GitKraken integration, or a stray manual command — the #1804/#300 pattern), git status/git push break in BOTH trees (every tracked file shows as deleted). An exhaustive audit confirmed no djust pre-push hook, test, or script writes core.bare — every in-repo git operation is read-only (git status/diff/grep/ls-files/rev-parse) or scoped to an isolated tmp dir (test_run_with_venv_python.py, test_git_commit_with_precommit.py, test_deploy_cli.py's git init fixtures), and all three were verified empirically to leave core.bare unchanged — so the corruption is external, not a framework bug. New scripts/check-shared-git-config.sh reads core.bare from the SHARED config (resolved via --git-common-dir, works from any worktree), reports a leak (exit 1), and with --fix performs the documented recovery (core.bare false); it NEVER writes core.bare true. The worktree-subagent mitigation (push --no-verify; CI is the authoritative gate) plus the detector are documented in CONTRIBUTING.md "Working in a git worktree". Tested by 5 cases in tests/test_check_shared_git_config.py (build a throwaway main+worktree, simulate the leak in the throwaway shared config, assert detect + --fix-recover + the never-writes-true invariant; gate-off self-tested per #1468).

  • Real type gaps surfaced flipping management/checks/auth/templatetags + loose modules to strict (ADR-023 M4d, group 1). None changed runtime behavior; each removes a latent contract lie. Convergence dividend (one real annotation bug, fixed):mixins/request.py's _streaming_iter was annotated AsyncIterator[str] but yields the ChunkEmitter's bytes chunks (the emitter encode("utf-8")s every chunk before queueing, and StreamingHttpResponse is fed bytes) — the strict typing of http_streaming.ChunkEmitter._aiter_impl() -> AsyncIterator[bytes] exposed the mismatch in the (already-strict M4c) mixins/request.py; corrected to AsyncIterator[bytes]. Other gaps (annotation-only, no behavior change):checks/security.pycheck_security's S002 @csrf_exempt scan read node.body[0].value.value (an ast.Constant.value, a str | bytes | int | … union) and called .lower() on it — guarded with isinstance(doc, str) so a non-str first-statement constant can't AttributeError (it never matched "csrf" anyway); _decorator_callable_name typed Optional[str] so the _is_permission_required_decorator comparison stops leaking Any. auth/core.pycheck_view_auth's login_url (a getattr(...) or getattr(...) over unstubbed Django) cast to str for the _check_django_access_mixins(login_url: str) contract; check_redis (djust_doctor) returns Optional[_CheckResult] (it returns None to skip the non-Redis path). Plus bool(...)/str(...)/cast(...) boundary narrowing at the Django-untyped surface (user.has_perms(...), apps.is_installed(...), self.style.SUCCESS(...), json.loads(...), template.render(...), click.prompt(...)) and dict[str, Any]/list[CheckMessage] var annotations where mixed-type literals were inferred too narrowly. cleanup_liveview_sessions imports the session helpers from their canonical source (djust.session_utils) instead of the live_view re-export so the strict island resolves them (equivalently exported via live_view.__all__).

  • Real type gaps surfaced flipping the theming/ subpackage to strict (ADR-023 M4c, part 3). None changed runtime behavior; each removes a latent contract lie, verified rendering byte-identical. theming/manager.py: ThemeState.pack was annotated str with a None default (a dataclass field lying about nullability — get_state() returns pack=None when no pack is configured), which also made the ThemeState(pack=pack) construction an [arg-type] error against str | None; corrected to str | None = None. theming/_registry_accessor.py: the ThemeRegistry singleton's _presets/_themes/_packs/_manifests/_discovered were assigned only through a local inst in __new__, so mypy saw 25 [attr-defined]/[has-type] errors at every access in _registry_accessor + registry — declared them as class-level annotations (dict[str, Any] / bool), the canonical singleton-attr fix (the attrs are still populated once per process in __new__). theming/theme_css_generator.py + theming/pack_css_generator.py: self.ds / self.pack were DesignSystem | None / ThemePack | None (the get_design_system/get_theme_pack return type) but __init__raises when None, so every later self.ds.typography / self.pack.icon_style access was a union-attr error (14 in pack, 8 in theme) — narrowed by assigning the post-raise non-None value to a self.ds: DesignSystem / self.pack: ThemePack annotated attr. theming/manager.py: the two CompleteThemeCSSGenerator reassignments (in generate_critical/deferred_css_for_state) collided with the inner ThemePackCSSGeneratorgen var's inferred type ([assignment] + a phantom [attr-defined] on generate_critical_css) — renamed the inner var pack_gen. theming/palette.py: s_h/a_h mixed int (from hex_to_hsl) and float (from params[hue_offset] % 360, where _MODE_PARAMS is inferred dict[str, float] because it mixes sat_scale=0.85 with integer hue offsets) — wrapped the (always-integer) hue offsets in int(...) to keep s_h/a_hint (int(180) % 360 is identical). theming/build_themes.py: build_all declared -> Dict[str, str] but artifacts["individual_themes"] is a list[str] — the return type lied about the heterogeneous shape; corrected to Dict[str, Any] (+ the manifest/artifacts locals annotated). theming/accessibility.py: a float ** float returned Any ([no-any-return]) and a bool-or chain over unstubbed-attr comparisons returned Any — wrapped in float(...)/bool(...). theming/mixins.py: the conditional-import event_handler fallback was an unguarded [no-redef] (narrow # type: ignore[no-redef]), _theme_manager was ThemeManager with a None default (corrected to ThemeManager | None), and the four event handlers gained if self._theme_manager is None: return guards so the _theme_manager.set_mode(...) accesses type-check (no-ops post-mount, matching _setup_theme_context's existing guard). Plus several mark_safe(...)/config-.get(...)/cookie-.get(...)Any-leaks narrowed at the boundary (cast(str, ...) / str(...)).

  • Real type gaps surfaced flipping the admin_ext/ subpackage to strict (ADR-023 M4c, part 2). None changed runtime behavior; each removes a latent contract lie, verified behavior-identical by the admin test suite. admin_ext/views.py: LoginView.update_username / update_password had implicit-Optional defaults (field: str = None) that PEP 484 prohibits — corrected to Optional[str]; and ModelCreateView.mount overrode ModelDetailView.mount(self, request, object_id=None, ...) with a narrower mount(self, request, **kwargs) signature (an LSP [override] violation) — restored the object_id parameter (still forced to None internally, so the create view's "always start with no object" behavior is unchanged) so the override is contract-compatible. admin_ext/options.py: several warn_return_any leaks at the Django-untyped boundary narrowed at the return site — _widget_has_permission returns bool(user.has_perms(...)), get_form pins modelform_factory(...) to a typed local, and get_field_display_name wraps the verbose_name/short_description reads in str(...). admin_ext/plugins.py: NavItem.has_permission / AdminWidget.has_permission / AdminWidget.render similarly narrowed (bool(request.user.has_perm(...)), str(render_to_string(...))). admin_ext/__init__.pyautodiscover and admin_ext/apps.pyDjustAdminConfig.ready gained explicit -> None returns.

  • Real type gaps surfaced flipping the mixins/ subpackage to strict (ADR-023 M4c, part 1). None changed runtime behavior; each removes a latent contract lie or surfaces a latent bug for follow-up. Latent bug (annotated, NOT fixed — out of scope #1079):mixins/components.pyComponentMixin.update_component calls component.update(**props) after isinstance(component, LiveComponent), but LiveComponent (which subclasses ContextProviderMixin, NOT Component) has no update method at runtime — confirmed via the live MRO (Component.update exists; LiveComponent.update does not). So update_component() would raise AttributeError if ever invoked with a LiveComponent. The strict flip carries a narrow # type: ignore[attr-defined] with a comment at the call site; the fix (move update onto LiveComponent, or change the routing) is left for a dedicated bugfix PR since mixins/ M4c(1) is annotation-only. Other gaps (annotation-only, no behavior change):mixins/page_metadata.py_pending_page_metadata / _drain_page_metadata were List[Dict] (incomplete generic) → List[Dict[str, str]]. mixins/model_binding.pyallowed_model_fields class attr was inferred None (from = None) → annotated Optional[List[str]] (the true subclass-override contract); _dj_model_fields bare frozensetfrozenset[str]. mixins/rust_bridge.pyrendered_context = {} was inferred Dict[str, dict[str, Any]] from its first (dict-valued) assignment, breaking later primitive/str assignments → annotated Dict[str, Any] (no logic change; the change-detection path is byte-identical). mixins/template.pypos = open_pos + 4 mixed the float("inf") sentinel into an int accumulator → narrowed int(open_pos) in the branch where open_pos < close_pos guarantees a real int; _current_html_size/_previous_html_size declared Optional[int] to match the getattr(..., None) first-render seed. mixins/jit.py_variable_extraction_cache was Dict[str, dict] but stores Optional[dict]Dict[str, Optional[dict]]; the if not extract_template_variables truthy-function check (a function is always truthy) → is None.

  • Real type gaps surfaced flipping the FINAL components/ modules to strict (ADR-023 M4b, part 3). None changed runtime behavior; each removes a latent contract lie, verified rendering byte-identical. components/components/button.py + components/ui/list_group_simple.py: Dict[str, any] (the builtin any function used as a type — a typo) corrected to Dict[str, Any]. components/ui/modal_simple.py: Modal._render_custom read self.show but __init__ never assigned it (the show= kwarg was passed to super().__init__ but not re-set as an instance attr like its siblings body/title/etc.) — [attr-defined] against the RustModal-instance path; added self.show = show to match the established pattern (byte-identical: in the Python-fallback render path the base already set it via its kwargs loop). components/components/prompt_editor.py: self.template reads were typed Optional[str] (the base Component.template: Optional[str] class attr) while the subclass always sets a str — narrowed via a template = self.template or "" local at the top of _render_custom. components/ui/navbar_simple.py: the nav-items param was annotated List[Dict[str, Union[str, bool, List[...]]]] which mis-typed the nested-dropdown access as non-iterable (union-attr on .get/__iter__) — widened to List[Dict[str, Any]] (the honest contract for heterogeneous dynamically-accessed dicts, #1108). Float/int local-init mismatches in chart/heatmap/pivot renderers (total = 00.0, y = ...y: float = ..., row_total/col_totals/grand_total → float) where a numeric accumulator was seeded int then +='d a float (output via :.1f/_format_val is identical). components/gallery/registry.py: cat = info.get("category", "misc") was object-typed (from the heterogeneous EXAMPLES literal) so cat.title() was attr-defined/call-overload — coerced cat = str(...) (category is always a str). Numerous list/dict locals across data_table/templatetags annotated to fix mixed-element var-annotated (e.g. pages: list mixing page ints and "...", col_items: list[list[Any]]).

  • Real type gaps surfaced flipping the components/ UI catalog to strict (ADR-023 M4b, part 2). None changed runtime behavior; each removes a latent contract lie. components/mixins/accordion.py + components/descriptors/accordion.py: AccordionState.active (and the descriptor's nested State.active) was annotated str, but in multiple=True mode it holds a list of open item ids — so actives.remove(value) / actives.append(value) / state.active = [value] were [attr-defined]/[assignment] errors against the declared str. Corrected to Union[str, List[str]] (the true runtime contract — single id when single, list when multiple), narrowing the list branch with cast(List[str], inst.active) (mixin, guarded by inst.multiple) / the existing isinstance(actives, list) (descriptor). The 8 deprecated state mixins (tooltip/tabs/sheet/modal/dropdown/collapsible/carousel/accordion) had component_id = self._resolve_component_id(component_id) reassign an Optional[str] return onto a now-str-typed param — coalesced to ... or "" (behavior-identical: _get_typed_instance("") and _get_typed_instance(None) both miss the instance dict and hit the inst is None guard). Typed all *_instances class vars (Optional[Dict[str, XState]]) and the descriptor _handle_event(self, state: "State", ...) params (the nested-State-subclass forward-ref, not the base TypedState, so state.is_visible/.active/etc. resolve).

  • Real type gaps surfaced flipping the components/ machinery to strict (ADR-023 M4b, part 1). None changed runtime behavior; each removes a latent contract lie. components/server_event_toast.py: ServerEventToastMixin.push_toast calls self.push_event(...), a method supplied by the host LiveView (via PushEventsMixin) and absent from the standalone mixin — mypy flagged [attr-defined]; declared the cooperating method under if TYPE_CHECKING: (the canonical djust mixin pattern, mirrors streaming.py), no runtime change. components/function_component.py: {% call %} dispatch set instance._slots / instance._children on a LiveComponent (per-invocation template-render attrs, distinct from the class-level slots declaration list) — narrow # type: ignore[attr-defined] with an explanatory comment; the @component decorator's _djust_* metadata stamps on a plain Callable likewise narrowed. components/presets.py: _BUTTON_PRESETS (heterogeneous str/bool values) was inferred dict[str, object], making the built-in register_preset(...) registration loop an [arg-type] error — annotated Dict[str, Dict[str, Any]]. components/mixins/base.py: TypedState.__init__ called default.fget(self) where property.fget is Optional — added the fget is not None guard (behavior-preserving for every real _make_property-built property). components/utils.py + components/icons.py + components/suspense.py + components/templatetags/_registry.py: several Any-leaks at Django boundaries (col.get(...)/value.strftime(...)/mark_safe(...)/conditional_escape(...)/render_to_string(...)) narrowed to str at the boundary so warn_return_any is satisfied without an Any escape.

  • Real type bugs surfaced flipping the loose top-level modules to strict (ADR-023 M4a). None changed runtime behavior; each removes a latent contract lie. react.py: ReactComponentRegistry._component_modules was annotated Dict[str, str] but every register() stores a nested {"module": ..., "export": ...} dict — the field annotation contradicted the (correct) return types of get_module_info() / get_all_modules(); corrected to Dict[str, Dict[str, str]]. presence.py: broadcast_to_presence(event, payload: Dict[str, Any] = None) declared a non-Optional param with a None default (the body already coalesces payload = {}) — corrected to Optional[Dict[str, Any]] = None. testing.py: assert_routed_views_allowed imported _routed_liveview_classes from djust.checks, where it is not re-exported (it lives in djust.checks.components) — corrected to import from the defining submodule (verified importable). performance.py: PerformanceTracker.root_node/current_node were inferred None-only from __init__ then reassigned TimingNode — declared Optional[TimingNode], and the _find_parent_node call now guards root_node (was guarding only current_node, but both are None/set together).

  • Real type gaps surfaced flipping the dispatch/runtime core to strict (ADR-023 M3). None changed runtime behavior; each makes the spine type-check clean and removes a latent contract lie. sse.py: SSESession._request was assigned (DjustSSEStreamView.get) and read (runtime.SSESessionTransport.build_request) but never declared in __init__ — added the Optional[Any] declaration alongside _event_request. runtime.py: _instantiate_error_frame was first-assigned None then a dict (mypy inferred None-only, so the dict assignments were errors) — declared Optional[Dict[str, Any]] in __init__; _instantiate_view called Optional[type]() ("None not callable") — added the ViewResolution.__bool__-implied view_class is None guard; the dormant actor-mount path called Optional[create_session_actor] — added the actor-availability guard. websocket.py: _recovery_html was str-typed from its first assignment but cleared to None on one-time use — annotated Optional[str]. live_view.pyi: the M2-island stub omitted the module-level _FRAMEWORK_INTERNAL_ATTRS that websocket._snapshot_assigns imports — added it (a stub-completeness gap the M3 flip surfaced because websocket now resolves the import against the strict stub). Several Any-leaks at Django/PyO3 boundaries narrowed at the boundary (bool()/str()/int() wraps on validate_host, child-render output, and the version helpers).

  • Real type bugs surfaced while building the mypy strict islands (ADR-023).security/attribute_guard.py: DANGEROUS_ATTRIBUTES was annotated Set[str] (mutable) but holds a frozenset — the annotation lied about mutability for a membership-only, never-mutated security denylist; corrected to frozenset[str], matching the immutable-denylist intent. security/log_sanitizer.py: sanitize_dict_for_log's result dict holds heterogeneous values (redacted strings, nested sanitized dicts, sanitized item lists) under an inferred dict[str, str] — annotated dict[str, Any]. security/state_snapshot.py (sign_snapshot) and permissions.py (dump_starter_document): [no-any-return] from untyped-dependency calls (TimestampSigner.sign, yaml.safe_dump) narrowed to str at the boundary. Plus annotation gaps closed in rate_limit, _context_provider, schema, permissions, and test_isolation (missing return/param annotations + var-annotated hints). None changed runtime behavior; they make the cited security/validation modules type-check clean under strict rules.

  • Real type bugs surfaced flipping the public-API quartet to strict (ADR-023 M2).decorators.event_handler: the untyped dual-call API (@event_handler bare vs @event_handler(...)) reported [arg-type] + "Self argument missing" at every bare-decorator call site (e.g. FormMixin.validate_field / submit_form) — the exact consumer-facing liability ADR-023 names; fixed with @overload so both forms type correctly for downstream consumers. live_view._is_serializable: _non_serializable was fixed to a 3-element tuple by inference, so the appended _thread.LockType (4th element) silently fell outside the declared type and the lock branch was effectively untyped — annotated tuple[type, ...]. live_view.pyi: the stream stub was missing the limit param present on StreamsMixin.stream (stub-vs-source signature drift, the #1646 class) — added. mixins/handlers.py: _handler_metadata had no base annotation, so its inferred non-optional dict conflicted with LiveView.__init__'s None init — annotated Optional[Dict[str, Dict[str, Any]]] to match the runtime contract (the is not None cache guard). decorators._ComputedProperty: custom metadata attrs (_is_computed, _computed_name, _computed_deps) were assigned but undeclared — declared as class annotations. None changed runtime behavior.

All releases · Atom feed