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/blockrender()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]]withignore_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 thatcasts Django's@keep_lazy-decorated (untyped →Any)mark_safetostr, absorbing the ~200-strongno-any-returncascade across every handler return without ignores; (b) inlinecast(...)/str(...)(runtime no-ops) at eachint()/float()/dict-key/attribute site of thekw.get(...) -> objectcascade, plus a handful of explicitvar: 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 (thecast/strcoercions are runtime no-ops; the onlystr()wraps that touch lookup keys were converted tocastto guarantee key identity).mypy python/djuststays GREEN (822 files) withdjust.components.rust_handlersstrict; gate-off-verified (#1468) — a wrong-typedintreturn injected into a handler (ModalHandler.render, declaredstr) 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. Seedocs/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]]withignore_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-enginetemplatetags/package already flipped in M4d group 1); and the theme-gallery / component-storybook surface (theming/gallery/—viewsthe gallery/editor/diff + storybook DEBUG/staff-gated views,contextthe example-context + token-serialization builders,component_registry,urls,storybook). None of the three subpackages has atests/dir, so the ratchet completes each in one PR with no test sub-package to defer. Annotated with real types (params + returns — notAnycosmetics):HttpRequest/HttpResponseon the gallery views,list[dict[str, Any]]on the example builders,Callable[[Type[TagHandler]], Type[TagHandler]]on the@registerdecorator factory. Render output is byte-identical — the SafeString/HTML boundaries (format_htmlinflash,escapeinmarkdown,Template.renderinpwa,reverseinurl,staticinstatic,_client_config_htmlinclient_config, the dynamic component.render()incomponent_registry) returnAnyunder the lenient global config (Django + the cross-islandlive_tags._client_config_htmlare seen as untyped), so each is coerced withstr(...)at the boundary to satisfywarn_return_anyWITHOUT changing the returned (already-safe) HTML. One real type fix:scaffolding/generator.pylist_display_fieldsannotatedlist[str](was an un-annotated[]flaggedvar-annotated).mypy python/djuststays GREEN (822 files) with all 20 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typedintreturn injected intotemplate_tags/url.UrlTagHandler.render(declaredstr) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. Seedocs/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 thedjust.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-levelColorScale/ThemeTokens/ThemePreset/DesignSystem/ThemePackliterals, zero functions), the dependency-free re-export hub_base, the package__init__(pure re-exports), and the deprecated_legacymodule (theTheme/THEMESdataclass 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 untypeddictoverrides (__getitem__/__contains__/get/items/keys/values/__iter__/__len__) — annotated to match thedict[str, Theme]superclass signatures (the three view methods declare-> Anyfor the un-nameable concretedict_items/dict_keys/dict_valuesreturn 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/djuststays GREEN (822 files) withtheming/themes/*strict; gate-off-verified (#1468) two ways — a wrong-typedstrreturn on_legacy._DeprecatedThemesDict.__len__(declaredint) 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-leveltheming/modules are already strict (M4c part 3), but mypy'sdjust.theming.*glob matches only direct children, not the deeperdjust.theming.themes.Xsubmodules, so this subpackage needed its own override entry. Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the loose top-level modules (appsDjustConfig,audit_astAST security-audit walker,audit_liveruntime auditor,bug_captureharness,checks_css_proposalproposed CSS system-checks,hookslifecycle registry,hot_view_replacementHVR engine,state_backend/template_backendback-compat re-export shims,template_filtershelpers,time_travelrecorder,utilsshared 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 — notAnycosmetics):db/decorators.notify_on_save.decoratetypedtype[models.Model]so_meta/labelresolve, with narrow# type: ignore[attr-defined]s on the dynamic_djust_notify_channel/_djust_notify_receiversintrospection attrs stashed on/deleted from the decorated model class; the signal receivers_on_save/_on_deleteannotated(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_backendnowcast(PresenceBackend, _registry.get())mirroring the already-strictstate_backends.registrypattern (the generic registry returnsAny);backends.redis.RedisPresenceBackend.countwraps the untypedzcountAny-return inint(...);db.notifications._import_psycopggained its-> tuple[Any, Any]return; anddb.notifications._dsn_from_url's URL-field loop variable was renamed (val→dsn_val) to stop colliding with the earlierstr-typedparse_qslloop var so the mixedstr | int | Nonefield tuple type-checks.mypy python/djuststays GREEN (822 files); gate-off-verified (#1468) — a wrong-typedintreturn inbackends.registry.get_presence_backend(declaredPresenceBackend) 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. Seedocs/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_componentswas 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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the four template-tag modules (theme_components— the ~26 themed component tagstheme_button/theme_card/theme_alert/theme_input/theme_modal/theme_table/theme_nav/etc.;theme_pages— the auth/error/utility page-fragment tagstheme_login_page/theme_404_page/theme_maintenance_page/etc.;theme_tags— thetheme_head/theme_css/theme_switcher/theme_preset/theme_modeaccessors + the sharedbuild_theme_head_contextbuilder;theme_form_tags—theme_form/theme_form_errors/get_css_prefix) and thedjust_thememanagement 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 theirmark_safe/format_htmlreturn values are annotatedSafeString(the HTML-safe boundary) andcontext/request/formparams getContext/HttpRequest | None/BaseForm; output is byte-identical. The management command uses the establishedCommandParser/*args: Any, **options: Anyshape mirroringdjust_setup_css/djust_doctor. Four real type fixes to clean the islands (the kind strict-flips surface, ADR-023):theme_components.theme_progressannotatespercentage: float(themin(100, (int(value)/int(max))*100)reassignment is afloat; the= 0seed inferredint→[assignment]);theme_tags.theme_framework_overridesnarrows theformat_htmlresult through astrlocal at the unstubbed-django boundary ([no-any-return]); the three_css_prefix()helpers +theme_pages._csrf_token_valuewrap the untypedget_theme_config().get(...)/get_token(...)boundary instr(...); anddjust_theme.handle_marketplace_inforeads the required-positionalmp_theme_namevia subscript (not.get()) so it stays non-Optionalfor thethemes_dir / theme_namePath division +get_component_coverage(str, ...)call ([operator]/[arg-type]).mypy python/djuststays GREEN (822 files) with all 8 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typedintreturn intheme_pages._css_prefix(declaredstr) turns the gate RED ([return-value]), reverting restores GREEN. Behavior is byte-identical (annotations + thestr(...)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 optionaltheming/gallerysubpackage, which remains for a continuation batch. Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the PWA layer (mixinsPWAMixin/OfflineMixin/SyncMixin,storageoffline backends +OfflineAction/SyncQueue,syncSyncManager/ConflictResolver,manifest,service_worker,utils), the optimization layer (fingerprintStateFingerprint/SectionCache/IncrementalStateSync,codegenserializer code-gen,query_optimizerselect/prefetch analysis,cacheSerializerCache,__init__), the multi-tenant layer (resolvers,managersTenantManager/TenantQuerySet,backendsredis/memory presence,middlewareContextVar tenant binding,mixinTenantMixin/TenantScopedMixin,audit,security,models,__init__— annotations only; tenant-isolation logic byte-identical), and the observability layer (viewslocalhost-gated endpoints,middlewarelocalhost gate,sql/timings/log_handler/tracebackscapture buffers,dry_runside-effect blocker,registry,urls,__init__). Annotated with real types (params + returns — notAnycosmetics), using the established mixin-collaborator pattern (# type: ignore[misc]on cooperativesuper().get_context_data()/dispatch()calls mirroringwizard.py/tenants;TYPE_CHECKING-onlypush_event/sync_queuestubs on the PWA mixins documenting the co-mixed-LiveViewcontract) and a narrow# type: ignore[import-untyped]ondry_run's lazyimport requests(a known-stub package mypy won't silence viaignore_missing_imports). Two real bugs fixed to clean the islands (the kind strict-flips surface, ADR-023):pwa.storage.OfflineAction.idwidened toUnion[str, int](callers forward an int model pk asobj_id; theSyncQueueaction-id params widened to match), andpwa.mixins.delete_offlinenow passes the requiredOfflineAction(data={})— omitting it raisedTypeErrorat runtime on every call (a guaranteed crash in an untested path).mypy python/djuststays GREEN (822 files) with all 31 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typedstrreturn inoptimization.fingerprint.StateFingerprint.version(declaredint) 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: thepwa/{templatetags,management}-style leaf packages do not exist for these four subpackages, so M4d(2) completes their non-test surface. Seedocs/adr/023-incremental-type-enforcement.md.Strict type enforcement on the
tutorials/,api/,template/, andstate_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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the declarative guided-tour state machine (tutorials/— theTutorialStepdataclass +TutorialMixinasync tour loop, withif 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_functiondispatch views, the pluggableBaseAuth/SessionAuthcontract, 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_contentsloaders, theDjustTemplaterendering pipeline incl. the{% extends %}/{% block %}parser +{% url %}resolver, and theserialize_value→JSONValueserializer), and the LiveView state-persistence backends (state_backends/— theStateBackendABC, the in-memory + Redis backends, and the registry).api/andstate_backends/are security/correctness-relevant — annotations only, logic byte-identical: the_snapshot_assigns/_compute_changed_keysdiff, 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.pyiwire-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-JITDjangoJSONEncoder = None/_get_model_hash = Noneimport fallbacks intemplate/rendering.py; the transientNone-view health-check probe entry instate_backends/memory.py);cast(...)is used at the Django/zstd/Rust unstubbed-boundaryAnyleaks, andassert ... is not Nonenarrows already-guarded optionals (thenext_start.end()block-parser sites, the_get_compressor()compress path gated by_compression_enabled).mypy python/djuststays GREEN (822 files) with all 20 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typedintreturn instate_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 atests/subdir, so the ratchet completes each in a single PR (no test sub-package to defer). Seedocs/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]]withignore_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_introspecthelper; the full Django system-check family (configuration/security/templates/quality/components/integrations/accessibility+ the sharedutils); the auth layer (thecheck_view_auth/run_pre_mount_auth/enforce_object_permissionsecurity core, theLoginRequiredLiveViewMixin/PermissionRequiredLiveViewMixin, thesocial_auth_providerscontext processor, the signup/loginviews+forms, and thedjust_adminplugin + itsOAuthProvidersView/SocialAccountsViewLiveView pages); all five template-tag modules (live_tags— the big one with{% live_render %}/{% colocated_hook %}/{% dj_activity %}+ the lazy-thunk emitter, plusdjust_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 — notAnycosmetics):SafeStringat themark_safe/format_htmlboundary;CheckMessagefor system-checkerrorslists + returns;argparse.Namespace/CommandParserfor the management commands;ast.*node types (ast.ClassDef/ast.Call/ast.expr/ast.Module) for the AST-based checks;AsyncIterator[bytes]for theChunkEmitterstreaming surface. The only narrow coded# type: ignores are at genuine dynamic edges: thedjust.checkssetattrre-export (_root.*— the patch-by-path contract from the #1822 monolith split), thedjust-adminoptional-dependency fallback class (no-redef/assignment), the optional_rustversionexport (not in the.pyi), the auth-mixin cooperativesuper().dispatch(provided by the combined View), and the Djangomodel._metaaccess (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/djuststays GREEN (822 files); gate-off-verified (#1468) — a wrong-typed return in a flipped module (templatetags/djust_flash.dj_flash→int) turns the gate RED ([no-any-return]), reverting restores GREEN. Full suite 8604 passed / 0 failed.requests(consumed bydeploy_cli) joinsyamlin the untyped-third-party override. Remaining for a continuation batch: themanagement/templatetags-adjacent long tail is already covered; thetenants/,backends/, andstate_backends/subpackages + the last few loose modules remain. Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the registry layer (_registry_accessorsingleton +registrydiscovery wiring), theThemeManager+ThemeStatestate/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,mixinsThemeMixin,components,formsrenderer), the build/adapters/tooling (build_themes,shadcn,tailwind,inspector,checks,manifest,loaders,theme_packs,compat,contracts), theappsAppConfig,views,urls, and the leaf_config/_constants/_types/_builtin_presetsmodules. Annotated with real types (params + returns — notAnycosmetics), using thecast(str, mark_safe(html))boundary pattern for the theme-component renderers (django'ssafestringis unstubbed, somark_safereturnsAny;SafeStringitself resolves toAnywithout django-stubs, so astrcast is the honest no-Any-leak shape).mypy python/djuststays GREEN (822 files) with all 39 strict and the rest lenient; gate-off-verified (#1468) — a wrong-typedintreturn inmanager.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 defensiveif self._theme_manager is None: returnguards in the fourThemeMixinevent handlers (no-ops on the real post-mount path, matching the existing_setup_theme_contextguard). Full suite 8604 passed / 0 failed; 1863 theming tests pass (incl. the previously-flakytest_theme_tags_rust_engine_1721, green via #1929's fixture). Remaining theming/ for a continuation batch: thetheming/{templatetags,management,gallery}subpackages (the templatetag modules are the heaviest —theme_components~51 errors — so they're a separate batch). Seedocs/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]]withignore_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 aTYPE_CHECKING-guarded import so the optionalmcpdep is never imported at module load,_ensure_django() -> bool,main() -> None, and the observability-tool returns); the binary-WebSocket-frame upload system (uploads/__init__— theUploadWriterbase +BufferedUploadWriter+UploadConfig+UploadManager,uploads/resumable— the resumable chunk protocol,uploads/storage— the in-memory + RedisUploadStateStoreimpls,uploads/views— theUploadStatusViewHTTP endpoint withHttpRequest/JsonResponseannotations); 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 atests/dir, so the ratchet completes in one PR with no test sub-package to defer. Annotated with real types (params + returns — notAnycosmetics); the only narrow coded edges are:# type: ignore[override]on the legacywrite_chunk(self, chunk)adapters (BufferedUploadWriter,GCSMultipartWriter,AzureBlockBlobWriter) — the dropped trailingchunk_indexdefault is an INTENTIONAL, runtime-dispatched part of theUploadWritercontract (_writer_accepts_chunk_indexintrospects the signature; documented on the base method), andcast(...)narrows at the untyped boundaries (json.loadsinuploads/storage,boto3.generate_presigned_urlins3_presigned,requests.Response.text+session.session_keyinmcp/server/uploads/views). Twomcp/serverobservability-toolparamsdicts inferred homogeneous-then-mutated-with-the-other-type were annotateddict[str, object]. Third-partyrequests(consumed bymcp/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/djuststays GREEN (822 files); gate-off-verified (#1468) — a wrong-typed return injected intouploads/storage.deleteturns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed (413 upload/mcp-related tests pass). Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans every non-testadmin_ext/module: theDjustAdminSite(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+ theadmin_login_requiredwrapper and the_VIEW_REGISTRYplumbing), theAdminFormMixin(FK/M2M option loading, date/time field detection, readonly handling, real-time field validation), the bulk-action progress widget +@admin_action_with_progressdecorator, theAdminTailwindAdapteradmin CSS-framework adapter, theregister/action/displaydecorators, theDjustAdminConfigAppConfig, the autodiscover package__init__, and the admin template-tag helpers (get_item/get_field/concat/admin_url). Excludesadmin_ext/tests/, which stays on the lenient global default. Annotated with real types (params + returns — notAnycosmetics):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: Anyannotation-only attrs onAdminBaseMixin+AdminFormMixindocumenting the co-mixed-LiveViewcontract, plus a# type: ignore[misc]on the cooperativesuper().as_view()mirroringwizard.py); decorator function-attribute stamping (wrapper.short_description = ...) carries narrow# type: ignore[attr-defined]at the genuine dynamic edge.mypy python/djuststays 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_adapters→int) 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. Seedocs/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 entiremixins/subpackage — the LiveView mixin layer that composes the publicLiveViewclass — from the lenient mypy default to strict islands ([[tool.mypy.overrides]]withignore_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_work—start_async/defer/assign_async,waiters—wait_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 (context—get_context_data/_apply_context_processors/_deep_serialize_dict,jit—_jit_serialize_queryset/_jit_serialize_model/_get_template_content), the HTTPrequestmixin (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 largetemplaterendering mixin (render/render_full_template/render_with_diff/arender_chunks+ the HTML extraction/stripping helpers). Annotated with real types (params + returns — notAnycosmetics), using the establishedif TYPE_CHECKING:host-attribute-declaration pattern (mirroringstreaming.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-RustRustLiveView = None/extract_template_variables = Noneimport fallbacks; theevent_handlerdirect-file-import fallback shim; the dynamiccomponent_id/_auto_idattribute sets on theComponent | LiveComponentunion).rust_bridge/jitchange-detection is byte-identical — annotations are runtime no-ops; the_sync_state_to_rustchange-detection, the_framework_attrs-class filter conventions, and all id()/value comparison logic are UNTOUCHED (no comparison or filter expression was altered).mypy python/djuststays GREEN (822 files); gate-off-verified (#1468) — injecting a wrong-typed return into a now-strict mixin (template.get_template→int) turns the gate RED ([return-value]), reverting restores GREEN. Full suite 8604 passed / 0 failed. Themixins/ratchet completes in a single PR (nomixins/tests/sub-package exists to defer). Seedocs/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]]withignore_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 — everyNode.render(self, args/content, context) -> SafeString,do_*(parser, token) -> template.Node, and@register.simple_tag/inclusion_tagfunction, with inclusion_tags correctly typed-> dict[str, Any]since they return a context dict, not HTML), the per-widgetmixins/data_table(theDataTableMixin— its ~21on_table_*event handlers,handle_*override hooks,get_*/_apply_*pipeline, and the safe-arithmetic expression parser), the gallery LiveView surface (gallery/live_views—GalleryCategoryMixin+ 9 category views, with thetemplate_nameLiskov conflict resolved by aTYPE_CHECKING-onlyLiveViewbase alias;views,examples,registry,context_processors, and thecomponent_gallerymanagement command), the ~24 remainingcomponents/components/*widgets with untyped private-helper params (_render_node/_squarify/_compute_diff/_eval_expression/etc.), thelayout/tabs/data/pagination/ttyd/terminalleaves, and theui/*_simplestateless widgets +ui/dropdown(the over-narrow nav-item dict widened to the honestAnycontract per #1108; the optional-Rust import shims —from djust._rust import RustX/RustX = Nonefallbacks 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 themark_safe(...) -> SafeStringboundary pattern (noAnyleak).rust_handlersis deliberately left LENIENT — it is a genuinely-dynamic Rust-bridge registry whose 193 handlers parse untyped Rust-engine arg lists intodict[str, object](thekw.get() -> objectcascade), so strict typing surfaces 344 errors (203no-any-return+ 91call-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/djuststays GREEN (823 files) with all 68 strict andrust_handlerslenient; gate-off-verified (#1468) — a wrong-typed return inmixins/data_table([return-value]) and a dropped annotation intemplatetags/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 documentedrust_handlersexception remains lenient within components/). Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any). Spans the fullcomponents/components/widget catalog (146 modules — alert, badge, card, spinner, kanban-adjacent leaves, charts, etc.), theui/stateless widgets (8 — spinner, modal, alert, progress, badge, button, card, list_group), thedata//forms//layout//gallery//management//ttyd/leaf packages, the descriptor-based components (descriptors/*— the DEP-002Accordion/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 — notAnycosmetics): typed*_instancesclass vars (Optional[Dict[str, XState]]),instance_id: str/component_id: str/is_open: boolparams,get_*_ctx(...) -> Dict[str, Any]accessors, andrender() -> SafeString(mirroring themarkdown.pyisland —mark_safe(...)returnsAnyunder django's unstubbedsafestring, soSafeStringis the correct str-compatible annotation that cleanly absorbs theAnywithout 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/djuststays 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-errormark_safe/kw.get()-objecticeberg, a separate decision), the per-widgetmixins/data_table, the big templatetag modules (templatetags/djust_components/_advanced/_forms/_charts), thegallery/live_views/views/examplesLiveViews, the ~24components/components/*widgets with untyped private-helper params (_render_node/_squarify/_compute_diff/etc.), and the union-typedui/*_simplewidgets +ui/navbar_simple/modal/dropdown(over-narrow dict inference + the declared-but-unbuiltRustNavBar/Rust*fallback imports). Seedocs/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]]withignore_errors = false+disallow_untyped_defs+disallow_incomplete_defs+warn_return_any):components.__init__,components.apps,components.registry(theLiveComponentname registry),components.assigns,components.dependencies(theDependencyManagerCSS/JS asset registry),components.function_component(the@componentdecorator +{% 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(sharedformat_cell/interpolate_color/interpolate_color_gradient+CURRENCY_SYMBOLS),components.mixins.base(the per-component interactive mixin base —ComponentMixin+ theTypedStatedict subclass),components.templatetags._registry(the sharedtemplate.Library+ the security-sensitivesafe_urlscheme-validator +_resolve/_parse_kv_args), andcomponents.templatetags._dev_tools(the Terminal/MarkdownEditor/JsonViewer/LogViewer/FileTree dev-tool template tags). Annotated with real types (params + returns — notAnycosmetics); the only narrow coded# type: ignore[attr-defined]are at genuine dynamic edges (the@componentdecorator stamping_djust_*metadata onto a plainCallable; the per-invocation_slots/_childrenattached to aLiveComponentinstance for template render). Themark_safe-returns-Anyboundary is handled with typed-local narrowing (a small_safe(html: str) -> strwrapper in_dev_tools,str-typed locals elsewhere) — noAnyleak.mypy python/djuststays 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-> stris added — themark_safe/kw.get()-objecticeberg), the per-widgetmixins.data_table, and the big templatetag modules (djust_components/_advanced/_forms/_charts), plus the UI catalog (ui/,data/,forms/,gallery/, charts UI). Seedocs/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]]withignore_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(thelive_sessionURLconf walk + auth-filtered route-map emit),formsets,simple_live_view,testing(the publicLiveViewTestClient+SnapshotTestMixin+LiveViewSmokeTestfuzz/smoke harness),react,rust_components,frameworks(the CSS framework adapters),js(theJScommand-chain builder),wizard(WizardMixin),performance,profiler, andpresence(PresenceMixin+LiveCursorMixin). Annotated with real types (params + returns — notAnycosmetics); the only# type: ignore[misc]are at genuine mixinsuper()-delegation edges (wizardmount/get_context_data, which the LiveView MRO supplies at runtime).mypy python/djuststays 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). Seedocs/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/ViewRuntimedispatch spine (every mount + event flows through them) are now mypy strict islands ([[tool.mypy.overrides]]withignore_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 — notAnycosmetics):runtime.py(42 strict errors —ViewRuntimedispatch helpers,_build_request/_check_auth/_extract_*, the actor-mount path,_tenant_context),websocket.py(73 —LiveViewConsumerlifecycleconnect/disconnect/receive, thehandle_*verb handlers, the Channels event handlersserver_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 — theDjustSSE*Viewget/postHTTP handlers, the owner-binding helpers, the SSE event-stream async generator),streaming.py(6 —StreamingMixin, withTYPE_CHECKINGhost-class attribute declarations), andwebsocket_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-binaryreceive()text frame).mypy python/djuststays 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). Seedocs/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 thatpy.typedexposes to downstream consumers are now mypy strict islands ([[tool.mypy.overrides]]withignore_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_viewdecorator + private-state helpers) and its PEP 561 stublive_view.pyi;components/base.py(theComponent+LiveComponentpublic bases — descriptor protocol, render waterfall, event-handler factory);decorators.py(@event_handler,@action,@server_function,@reactive,@state,@computed,@optimistic,@background+ their nested wrappers/descriptors); andforms.py(FormMixin+LiveViewForm).mypy python/djuststays 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). Seedocs/adr/023-incremental-type-enforcement.md.Enforced incremental type-checking — a mypy merge gate + strict islands + the
_rust.pyiboundary (ADR-023). djust shipspy.typed(PEP 561 — downstream consumers type-check against djust's hints), andpyproject.tomldeclared a strict[tool.mypy]config — but mypy was invoked nowhere (CI / Makefile / pre-commit), so the strict config was dead andmypy python/djustreported 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]]withignore_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), plusrate_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-errormypystep in thepython-testsCI job (a new MERGE GATE — #1236 governance — wired into thetest-summaryAND-condition; ships gating because it is green by construction, per #1534), amake typechecktarget (inmake check), and a scoped pre-commit hook onpython/djust/**.py{,i}changes. The_rust.pyistub'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) sincepy.typedexposes it. Seedocs/adr/023-incremental-type-enforcement.md.
Changed
CI: CodeQL config excludes
py/ineffectual-statement(false-positive noise from the ADR-023TYPE_CHECKINGstub idiom). The strict-mypy ratchet addedif 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'spy/ineffectual-statementflags 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'squery-filtersso it doesn't recur as the type-checking blocks grow. Also converted acast("Any", …)string forward-ref to a directcast(Any, …)incomponents/rust_handlers.pyso CodeQL sees theAnyimport 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.gotoflake on the BLOCKING browser-smoke gate (#1943). The blocking browser-smoke gate (#1869) intermittently red-barred unrelated PRs withPage.goto: Timeout 30000ms exceedednavigating to/demos/browser-smoke/: on a cold-cache run that compiles thedjust_componentsRust crate from scratch, the demo uvicorn server wasn't ready within the fixed 30spage.gotodeadline (it cleared on a warm-cache re-run — not a real break). The.github/actions/djust-playwright-serverreadiness 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/, usingcurl -fsSso a half-initialized app's 5xx does NOT count as ready — this warms the route (first-hit lazy import/compilation) DURING the bounded loop sopage.gotolands on an already-warm route instead of racing its own deadline.tests/playwright/test_browser_smoke.py'spage.gotoalso 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.logdump) 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>) — CodeQLpy/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</tagfollowed 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). NewTestEndTagWhitespacePreservation(whitespace + newline + bogus-attribute cases, gate-off verified) intest_strip_whitespace.py._run_async_workno longer writes against a stale view on disconnect/re-mount mid-await (#1940).LiveViewConsumer._run_async_workruns as a detachedensure_futuretask that capturesview = self.view_instancebefore its firstawait(the background callback). If adisconnect(which nullsview_instance) or alive_redirect/ re-mount (which reassignsview_instanceto a NEW view) interleaved during that await window, the completed task ran itshandle_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_asyncruns 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 inTestRunAsyncWorkTeardown(python/djust/tests/test_run_async_work_teardown_1940.py), gate-off verified.TutorialMixinnow initializes its four internal tutorial-signal attrs in__init__, not thetutorial_total_stepssetter (#1952)._tutorial_active_target,_tutorial_active_class,_tutorial_skip_signal, and_tutorial_cancel_signalwere previously initialized inside thetutorial_total_stepsSETTER, so aTutorialMixinview that never settutorial_total_steps(or read those attrs before the setter ran) hitAttributeError— e.g._cleanup_active_step()(called fromstart_tutorial'sfinallyblock) reads_tutorial_active_target/_class, andskip_tutorial/cancel_tutorialread the skip/cancel signals once running. The four attrs now default toNonein__init__(placed alongside the existing_tutorial_running/_tutorial_current_step/_tutorial_total_stepsinits); 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 inTestSignalAttrsInitializedInInit(read the four signals without invoking the setter; pre-fix raisedAttributeError).ComponentMixin.update_componentno longer raisesAttributeErroron aLiveComponent(#1947).update_component(component_id, **props)callscomponent.update(**props), butLiveComponent(a subclass ofContextProviderMixin, NOTComponent) had noupdate()method at runtime, so anyLiveComponentthat did not define its ownupdate()raisedAttributeErroron that path (the latent bug annotated with a# type: ignore[attr-defined]in ADR-023 M4c, part 1).LiveComponentnow has a baseupdate(**kwargs)that sets each prop as an instance attribute (mirroring the Python/hybrid path ofComponent.update) and returnsselffor chaining — the samecomponent.update(**props)API the component docs already document. Subclassupdate()overrides still take precedence. The stale# type: ignore[attr-defined]at the call site is removed. Regression coverage inTestUpdateComponentNoUpdateOverride(tests/unit/test_component_parent_communication.py): bare-LiveComponent update throughupdate_component, base-update()chaining returns self, subclass-override-still-wins.Dev-env: detect + recover the
core.bare = trueshared-config corruption that breaks worktree + main checkout (#1938). A linkedgit worktreeshares one.git/configwith the main checkout; if anything flipscore.baretotruethere (a build/PyO3-repoint step runninggit config core.bare true, an IDE/GitKraken integration, or a stray manual command — the #1804/#300 pattern),git status/git pushbreak in BOTH trees (every tracked file shows as deleted). An exhaustive audit confirmed no djust pre-push hook, test, or script writescore.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'sgit initfixtures), and all three were verified empirically to leavecore.bareunchanged — so the corruption is external, not a framework bug. Newscripts/check-shared-git-config.shreadscore.barefrom the SHARED config (resolved via--git-common-dir, works from any worktree), reports a leak (exit 1), and with--fixperforms the documented recovery (core.bare false); it NEVER writescore.bare true. The worktree-subagent mitigation (push--no-verify; CI is the authoritative gate) plus the detector are documented in CONTRIBUTING.md "Working in agit worktree". Tested by 5 cases intests/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_iterwas annotatedAsyncIterator[str]but yields theChunkEmitter'sbyteschunks (the emitterencode("utf-8")s every chunk before queueing, andStreamingHttpResponseis fed bytes) — the strict typing ofhttp_streaming.ChunkEmitter._aiter_impl() -> AsyncIterator[bytes]exposed the mismatch in the (already-strict M4c)mixins/request.py; corrected toAsyncIterator[bytes]. Other gaps (annotation-only, no behavior change):checks/security.pycheck_security's S002@csrf_exemptscan readnode.body[0].value.value(anast.Constant.value, astr | bytes | int | …union) and called.lower()on it — guarded withisinstance(doc, str)so a non-str first-statement constant can'tAttributeError(it never matched"csrf"anyway);_decorator_callable_nametypedOptional[str]so the_is_permission_required_decoratorcomparison stops leakingAny.auth/core.pycheck_view_auth'slogin_url(agetattr(...) or getattr(...)over unstubbed Django)casttostrfor the_check_django_access_mixins(login_url: str)contract;check_redis(djust_doctor) returnsOptional[_CheckResult](it returnsNoneto skip the non-Redis path). Plusbool(...)/str(...)/cast(...)boundary narrowing at the Django-untyped surface (user.has_perms(...),apps.is_installed(...),self.style.SUCCESS(...),json.loads(...),template.render(...),click.prompt(...)) anddict[str, Any]/list[CheckMessage]var annotations where mixed-type literals were inferred too narrowly.cleanup_liveview_sessionsimports the session helpers from their canonical source (djust.session_utils) instead of thelive_viewre-export so the strict island resolves them (equivalently exported vialive_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.packwas annotatedstrwith aNonedefault (a dataclass field lying about nullability —get_state()returnspack=Nonewhen no pack is configured), which also made theThemeState(pack=pack)construction an[arg-type]error againststr | None; corrected tostr | None = None.theming/_registry_accessor.py: theThemeRegistrysingleton's_presets/_themes/_packs/_manifests/_discoveredwere assigned only through a localinstin__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.packwereDesignSystem | None/ThemePack | None(theget_design_system/get_theme_packreturn type) but__init__raises when None, so every laterself.ds.typography/self.pack.icon_styleaccess was aunion-attrerror (14 in pack, 8 in theme) — narrowed by assigning the post-raisenon-None value to aself.ds: DesignSystem/self.pack: ThemePackannotated attr.theming/manager.py: the twoCompleteThemeCSSGeneratorreassignments (ingenerate_critical/deferred_css_for_state) collided with the innerThemePackCSSGeneratorgenvar's inferred type ([assignment]+ a phantom[attr-defined]ongenerate_critical_css) — renamed the inner varpack_gen.theming/palette.py:s_h/a_hmixedint(fromhex_to_hsl) andfloat(fromparams[hue_offset] % 360, where_MODE_PARAMSis inferreddict[str, float]because it mixessat_scale=0.85with integer hue offsets) — wrapped the (always-integer) hue offsets inint(...)to keeps_h/a_hint(int(180) % 360is identical).theming/build_themes.py:build_alldeclared-> Dict[str, str]butartifacts["individual_themes"]is alist[str]— the return type lied about the heterogeneous shape; corrected toDict[str, Any](+ themanifest/artifactslocals annotated).theming/accessibility.py: afloat ** floatreturnedAny([no-any-return]) and abool-orchain over unstubbed-attr comparisons returnedAny— wrapped infloat(...)/bool(...).theming/mixins.py: the conditional-importevent_handlerfallback was an unguarded[no-redef](narrow# type: ignore[no-redef]),_theme_managerwasThemeManagerwith aNonedefault (corrected toThemeManager | None), and the four event handlers gainedif self._theme_manager is None: returnguards so the_theme_manager.set_mode(...)accesses type-check (no-ops post-mount, matching_setup_theme_context's existing guard). Plus severalmark_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_passwordhad implicit-Optional defaults (field: str = None) that PEP 484 prohibits — corrected toOptional[str]; andModelCreateView.mountoverrodeModelDetailView.mount(self, request, object_id=None, ...)with a narrowermount(self, request, **kwargs)signature (an LSP[override]violation) — restored theobject_idparameter (still forced toNoneinternally, so the create view's "always start with no object" behavior is unchanged) so the override is contract-compatible.admin_ext/options.py: severalwarn_return_anyleaks at the Django-untyped boundary narrowed at the return site —_widget_has_permissionreturnsbool(user.has_perms(...)),get_formpinsmodelform_factory(...)to a typed local, andget_field_display_namewraps theverbose_name/short_descriptionreads instr(...).admin_ext/plugins.py:NavItem.has_permission/AdminWidget.has_permission/AdminWidget.rendersimilarly narrowed (bool(request.user.has_perm(...)),str(render_to_string(...))).admin_ext/__init__.pyautodiscoverandadmin_ext/apps.pyDjustAdminConfig.readygained explicit-> Nonereturns.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_componentcallscomponent.update(**props)afterisinstance(component, LiveComponent), butLiveComponent(which subclassesContextProviderMixin, NOTComponent) has noupdatemethod at runtime — confirmed via the live MRO (Component.updateexists;LiveComponent.updatedoes not). Soupdate_component()would raiseAttributeErrorif ever invoked with aLiveComponent. The strict flip carries a narrow# type: ignore[attr-defined]with a comment at the call site; the fix (moveupdateontoLiveComponent, or change the routing) is left for a dedicated bugfix PR sincemixins/M4c(1) is annotation-only. Other gaps (annotation-only, no behavior change):mixins/page_metadata.py_pending_page_metadata/_drain_page_metadatawereList[Dict](incomplete generic) →List[Dict[str, str]].mixins/model_binding.pyallowed_model_fieldsclass attr was inferredNone(from= None) → annotatedOptional[List[str]](the true subclass-override contract);_dj_model_fieldsbarefrozenset→frozenset[str].mixins/rust_bridge.pyrendered_context = {}was inferredDict[str, dict[str, Any]]from its first (dict-valued) assignment, breaking later primitive/str assignments → annotatedDict[str, Any](no logic change; the change-detection path is byte-identical).mixins/template.pypos = open_pos + 4mixed thefloat("inf")sentinel into anintaccumulator → narrowedint(open_pos)in the branch whereopen_pos < close_posguarantees a real int;_current_html_size/_previous_html_sizedeclaredOptional[int]to match thegetattr(..., None)first-render seed.mixins/jit.py_variable_extraction_cachewasDict[str, dict]but storesOptional[dict]→Dict[str, Optional[dict]]; theif not extract_template_variablestruthy-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 builtinanyfunction used as a type — a typo) corrected toDict[str, Any].components/ui/modal_simple.py:Modal._render_customreadself.showbut__init__never assigned it (theshow=kwarg was passed tosuper().__init__but not re-set as an instance attr like its siblingsbody/title/etc.) —[attr-defined]against theRustModal-instance path; addedself.show = showto 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.templatereads were typedOptional[str](the baseComponent.template: Optional[str]class attr) while the subclass always sets astr— narrowed via atemplate = self.template or ""local at the top of_render_custom.components/ui/navbar_simple.py: the nav-itemsparam was annotatedList[Dict[str, Union[str, bool, List[...]]]]which mis-typed the nested-dropdownaccess as non-iterable (union-attron.get/__iter__) — widened toList[Dict[str, Any]](the honest contract for heterogeneous dynamically-accessed dicts, #1108). Float/int local-init mismatches in chart/heatmap/pivot renderers (total = 0→0.0,y = ...→y: float = ...,row_total/col_totals/grand_total→ float) where a numeric accumulator was seededintthen+='d a float (output via:.1f/_format_valis identical).components/gallery/registry.py:cat = info.get("category", "misc")wasobject-typed (from the heterogeneous EXAMPLES literal) socat.title()wasattr-defined/call-overload— coercedcat = str(...)(category is always a str). Numerous list/dict locals across data_table/templatetags annotated to fix mixed-elementvar-annotated(e.g.pages: listmixing 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 nestedState.active) was annotatedstr, but inmultiple=Truemode it holds a list of open item ids — soactives.remove(value)/actives.append(value)/state.active = [value]were[attr-defined]/[assignment]errors against the declaredstr. Corrected toUnion[str, List[str]](the true runtime contract — single id when single, list when multiple), narrowing the list branch withcast(List[str], inst.active)(mixin, guarded byinst.multiple) / the existingisinstance(actives, list)(descriptor). The 8 deprecated state mixins (tooltip/tabs/sheet/modal/dropdown/collapsible/carousel/accordion) hadcomponent_id = self._resolve_component_id(component_id)reassign anOptional[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 theinst is Noneguard). Typed all*_instancesclass vars (Optional[Dict[str, XState]]) and the descriptor_handle_event(self, state: "State", ...)params (the nested-State-subclass forward-ref, not the baseTypedState, sostate.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_toastcallsself.push_event(...), a method supplied by the hostLiveView(viaPushEventsMixin) and absent from the standalone mixin — mypy flagged[attr-defined]; declared the cooperating method underif TYPE_CHECKING:(the canonical djust mixin pattern, mirrorsstreaming.py), no runtime change.components/function_component.py:{% call %}dispatch setinstance._slots/instance._childrenon aLiveComponent(per-invocation template-render attrs, distinct from the class-levelslotsdeclaration list) — narrow# type: ignore[attr-defined]with an explanatory comment; the@componentdecorator's_djust_*metadata stamps on a plainCallablelikewise narrowed.components/presets.py:_BUTTON_PRESETS(heterogeneousstr/boolvalues) was inferreddict[str, object], making the built-inregister_preset(...)registration loop an[arg-type]error — annotatedDict[str, Dict[str, Any]].components/mixins/base.py:TypedState.__init__calleddefault.fget(self)whereproperty.fgetisOptional— added thefget is not Noneguard (behavior-preserving for every real_make_property-built property).components/utils.py+components/icons.py+components/suspense.py+components/templatetags/_registry.py: severalAny-leaks at Django boundaries (col.get(...)/value.strftime(...)/mark_safe(...)/conditional_escape(...)/render_to_string(...)) narrowed tostrat the boundary sowarn_return_anyis satisfied without anAnyescape.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_moduleswas annotatedDict[str, str]but everyregister()stores a nested{"module": ..., "export": ...}dict — the field annotation contradicted the (correct) return types ofget_module_info()/get_all_modules(); corrected toDict[str, Dict[str, str]].presence.py:broadcast_to_presence(event, payload: Dict[str, Any] = None)declared a non-Optionalparam with aNonedefault (the body already coalescespayload = {}) — corrected toOptional[Dict[str, Any]] = None.testing.py:assert_routed_views_allowedimported_routed_liveview_classesfromdjust.checks, where it is not re-exported (it lives indjust.checks.components) — corrected to import from the defining submodule (verified importable).performance.py:PerformanceTracker.root_node/current_nodewere inferredNone-only from__init__then reassignedTimingNode— declaredOptional[TimingNode], and the_find_parent_nodecall now guardsroot_node(was guarding onlycurrent_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._requestwas assigned (DjustSSEStreamView.get) and read (runtime.SSESessionTransport.build_request) but never declared in__init__— added theOptional[Any]declaration alongside_event_request.runtime.py:_instantiate_error_framewas first-assignedNonethen a dict (mypy inferredNone-only, so the dict assignments were errors) — declaredOptional[Dict[str, Any]]in__init__;_instantiate_viewcalledOptional[type]()("None not callable") — added theViewResolution.__bool__-impliedview_class is Noneguard; the dormant actor-mount path calledOptional[create_session_actor]— added the actor-availability guard.websocket.py:_recovery_htmlwasstr-typed from its first assignment but cleared toNoneon one-time use — annotatedOptional[str].live_view.pyi: the M2-island stub omitted the module-level_FRAMEWORK_INTERNAL_ATTRSthatwebsocket._snapshot_assignsimports — added it (a stub-completeness gap the M3 flip surfaced because websocket now resolves the import against the strict stub). SeveralAny-leaks at Django/PyO3 boundaries narrowed at the boundary (bool()/str()/int()wraps onvalidate_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_ATTRIBUTESwas annotatedSet[str](mutable) but holds afrozenset— the annotation lied about mutability for a membership-only, never-mutated security denylist; corrected tofrozenset[str], matching the immutable-denylist intent.security/log_sanitizer.py:sanitize_dict_for_log'sresultdict holds heterogeneous values (redacted strings, nested sanitized dicts, sanitized item lists) under an inferreddict[str, str]— annotateddict[str, Any].security/state_snapshot.py(sign_snapshot) andpermissions.py(dump_starter_document):[no-any-return]from untyped-dependency calls (TimestampSigner.sign,yaml.safe_dump) narrowed tostrat the boundary. Plus annotation gaps closed inrate_limit,_context_provider,schema,permissions, andtest_isolation(missing return/param annotations +var-annotatedhints). 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_handlerbare 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@overloadso both forms type correctly for downstream consumers.live_view._is_serializable:_non_serializablewas 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 — annotatedtuple[type, ...].live_view.pyi: thestreamstub was missing thelimitparam present onStreamsMixin.stream(stub-vs-source signature drift, the #1646 class) — added.mixins/handlers.py:_handler_metadatahad no base annotation, so its inferred non-optionaldictconflicted withLiveView.__init__'sNoneinit — annotatedOptional[Dict[str, Dict[str, Any]]]to match the runtime contract (theis not Nonecache guard).decorators._ComputedProperty: custom metadata attrs (_is_computed,_computed_name,_computed_deps) were assigned but undeclared — declared as class annotations. None changed runtime behavior.