refactor: typed PanelApp/ThreadList/ThreadView dispatch protocols #15

Merged
clanker merged 2 commits from pr/refactor-protocols into master 2026-08-14 19:59:02 +00:00
Member

Refactor: structural protocols replace object + hasattr dispatch.

The codebase had two silent-failure risks: panels typed their app as object (24 mypy errors — every status_message/refresh_panels/update_single_thread call was unchecked), and the controller dispatched keymap actions via hasattr() strings where a renamed method would silently no-op.

  • protocols.py (new) — PanelApp (the interface both Dodo and AppController satisfy), plus runtime_checkable ThreadList / ThreadView for the tab/preview method sets, and the LIST_METHODS / THREAD_METHODS allowlists.
  • controller — navigate_list, mark_and_advance, toggle_tag_hotkey, delegate_to_list, delegate_to_thread now narrow via isinstance (structural) and log unknown method names instead of silently no-oping.
  • panels/mixin/webengine/commandbar — app: object → PanelApp; MessagePage widened from Dodo to PanelApp (cast for the QWebEngineProfile parent, which needs a QObject).
  • keymap — all keymap dicts annotated (Keymap = Dict[str, Tuple[str, Callable[..., Any]]]), fixing the lambda inference error at the 1–9 hotkey loop.

Verified headless: SearchPanel satisfies ThreadList structurally, TagPanel does not; dispatch works on a real panel; unknown methods fail fast with a warning; every delegate_to_list/delegate_to_thread string in the keymap is validated against the allowlists.

mypy: 182 → 152 (30 fixed, zero new).

**Refactor: structural protocols replace `object` + `hasattr` dispatch.** The codebase had two silent-failure risks: panels typed their app as `object` (24 mypy errors — every `status_message`/`refresh_panels`/`update_single_thread` call was unchecked), and the controller dispatched keymap actions via `hasattr()` strings where a renamed method would silently no-op. - **`protocols.py` (new)** — `PanelApp` (the interface both `Dodo` and `AppController` satisfy), plus `runtime_checkable` `ThreadList` / `ThreadView` for the tab/preview method sets, and the `LIST_METHODS` / `THREAD_METHODS` allowlists. - **controller** — `navigate_list`, `mark_and_advance`, `toggle_tag_hotkey`, `delegate_to_list`, `delegate_to_thread` now narrow via `isinstance` (structural) and **log unknown method names** instead of silently no-oping. - **panels/mixin/webengine/commandbar** — `app: object` → `PanelApp`; `MessagePage` widened from `Dodo` to `PanelApp` (cast for the `QWebEngineProfile` parent, which needs a `QObject`). - **keymap** — all keymap dicts annotated (`Keymap = Dict[str, Tuple[str, Callable[..., Any]]]`), fixing the lambda inference error at the 1–9 hotkey loop. Verified headless: `SearchPanel` satisfies `ThreadList` structurally, `TagPanel` does not; dispatch works on a real panel; unknown methods fail fast with a warning; every `delegate_to_list`/`delegate_to_thread` string in the keymap is validated against the allowlists. mypy: **182 → 152** (30 fixed, zero new).
- close_panel(0): 'if not to_close:' treated index 0 as falsy and
  closed the *current* tab instead of tab 0. Now 'to_close is None'
  distinguishes 'no argument' from an explicit 0.
- headerData: section <= len(columns) was off-by-one; now <.
- keymap: 'D' was a duplicate of 'I' (both show inbox); removed D,
  updated the roadmap docs.
- setup.py: python_requires >=3.7 -> >=3.10 to match the code (X | None,
  typing.Literal, modern PyQt6) and the README/.agent requirement.
Every single-message tag change (u/f/s in the preview) used to trigger
beginResetModel + a notmuch search in *every* open search panel: the
tree was torn down, selection/scroll lost, and the view re-rendered.
refresh_thread now replaces the row and emits dataChanged when the
thread still matches the query; only falls back to a full reset when
the thread drops out of the results (row removal) or notmuch errors.

Verified: no modelReset on in-place update, no signals when nothing
changed, full reset when the thread leaves the query.
Panels and keymaps now dispatch through structural protocols instead of
'object' + hasattr chains that failed silently on typos.

- protocols.py (new): PanelApp (satisfied by Dodo and AppController),
  runtime_checkable ThreadList / ThreadView, and the method-name
  allowlists used by the string-based delegators.
- controller: navigate_list / mark_and_advance / toggle_tag_hotkey /
  delegate_to_list / delegate_to_thread narrow via isinstance() and log
  unknown method names instead of no-opping; typed calls replace the
  hasattr() checks.
- Panel / panels / mixin: 'app: object' -> PanelApp; MessagePage and
  CommandBar widened likewise.
- keymap: all keymap dicts annotated (fixes the lambda inference error).

mypy: 182 -> 152 errors (30 fixed: the app:object bucket + mixin app
conflict), zero new.
Nothing panel-side uses them; the controller mirrors are being removed
in a separate PR, so requiring them would break AppController's
structural compatibility.
clanker force-pushed pr/refactor-protocols from 258b26c42e to 975eddfa07 2026-08-14 19:56:53 +00:00 Compare
clanker merged commit cf8236c52a into master 2026-08-14 19:59:02 +00:00
clanker deleted branch pr/refactor-protocols 2026-08-14 19:59:03 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Home/lazarus!15
No description provided.