From 3e2b9e40be0d14465b4374e0a0a5cfc7e224d0fd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mateusz=20=C5=BBebrak?= <mzebrak@syncad.com>
Date: Fri, 7 Mar 2025 14:47:50 +0100
Subject: [PATCH] Allow for no context in Form/FormScreen

---
 clive/__private/ui/forms/form.py         | 29 +++++++++++++-----------
 clive/__private/ui/forms/form_context.py | 12 ++++++++++
 clive/__private/ui/forms/form_screen.py  | 11 +++++----
 3 files changed, 34 insertions(+), 18 deletions(-)
 create mode 100644 clive/__private/ui/forms/form_context.py

diff --git a/clive/__private/ui/forms/form.py b/clive/__private/ui/forms/form.py
index a1ee9ec61c..954e87107f 100644
--- a/clive/__private/ui/forms/form.py
+++ b/clive/__private/ui/forms/form.py
@@ -7,8 +7,9 @@ from queue import Queue
 from typing import TYPE_CHECKING, Any
 
 from clive.__private.core.commands.abc.command import Command
-from clive.__private.core.contextual import ContextT, Contextual
+from clive.__private.core.contextual import ContextualHolder
 from clive.__private.ui.clive_screen import CliveScreen
+from clive.__private.ui.forms.form_context import FormContextT, NoContext
 
 if TYPE_CHECKING:
     from clive.__private.ui.forms.form_screen import FormScreenBase
@@ -16,38 +17,37 @@ if TYPE_CHECKING:
 PostAction = Command | Callable[[], Any]
 
 
-class Form(Contextual[ContextT], CliveScreen[None]):
+class Form(ContextualHolder[FormContextT], CliveScreen[None]):
     MINIMUM_SCREEN_COUNT = 2  # Rationale: it makes no sense to have only one screen in the form
 
     def __init__(self) -> None:
         self._current_screen_index = 0
-        self._screen_types: list[type[FormScreenBase[ContextT]]] = [*list(self.compose_form())]
+        self._screen_types: list[type[FormScreenBase[FormContextT]]] = [*list(self.compose_form())]
         assert len(self._screen_types) >= self.MINIMUM_SCREEN_COUNT, "Form must have at least 2 screens"
-        self._rebuild_context()
         self._post_actions = Queue[PostAction]()
 
-        super().__init__()
+        super().__init__(self._build_context())
 
     @abstractmethod
-    def compose_form(self) -> Iterator[type[FormScreenBase[ContextT]]]:
+    def compose_form(self) -> Iterator[type[FormScreenBase[FormContextT]]]:
         """Yield screens types in the order they should be displayed."""
 
-    @abstractmethod
-    def _rebuild_context(self) -> None:
-        """Create brand new fresh context."""
-
     @property
-    def screens_types(self) -> list[type[FormScreenBase[ContextT]]]:
+    def screens_types(self) -> list[type[FormScreenBase[FormContextT]]]:
         return self._screen_types
 
     @property
-    def current_screen_type(self) -> type[FormScreenBase[ContextT]]:
+    def current_screen_type(self) -> type[FormScreenBase[FormContextT]]:
         return self._screen_types[self._current_screen_index]
 
-    def on_mount(self) -> None:
+    async def on_mount(self) -> None:
         assert self._current_screen_index == 0
+        await self.initialize()
         self._push_current_screen()
 
+    async def initialize(self) -> None:
+        """Do actions that should be executed before the first form is displayed."""
+
     def next_screen(self) -> None:
         if not self._check_valid_range(self._current_screen_index + 1):
             return
@@ -83,6 +83,9 @@ class Form(Contextual[ContextT], CliveScreen[None]):
             else:
                 action()
 
+    def _build_context(self) -> FormContextT:
+        return NoContext()  # type: ignore[return-value]
+
     def _push_current_screen(self) -> None:
         self.app.push_screen(self.current_screen_type(self))
 
diff --git a/clive/__private/ui/forms/form_context.py b/clive/__private/ui/forms/form_context.py
new file mode 100644
index 0000000000..1fe6e80e7f
--- /dev/null
+++ b/clive/__private/ui/forms/form_context.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from typing_extensions import TypeVar
+
+from clive.__private.core.contextual import Context
+
+
+class NoContext(Context):
+    """A class that signals that there is no context."""
+
+
+FormContextT = TypeVar("FormContextT", bound=Context, default=NoContext)
diff --git a/clive/__private/ui/forms/form_screen.py b/clive/__private/ui/forms/form_screen.py
index 10f0bc7c7f..8dea9780c0 100644
--- a/clive/__private/ui/forms/form_screen.py
+++ b/clive/__private/ui/forms/form_screen.py
@@ -10,8 +10,9 @@ from textual.message import Message
 from textual.reactive import var
 
 from clive.__private.core.constants.tui.bindings import NEXT_SCREEN_BINDING_KEY, PREVIOUS_SCREEN_BINDING_KEY
-from clive.__private.core.contextual import ContextT, Contextual
+from clive.__private.core.contextual import Contextual
 from clive.__private.ui.clive_screen import CliveScreen
+from clive.__private.ui.forms.form_context import FormContextT
 from clive.__private.ui.forms.navigation_buttons import NextScreenButton, PreviousScreenButton
 from clive.__private.ui.widgets.inputs.clive_input import CliveInput
 
@@ -22,17 +23,17 @@ if TYPE_CHECKING:
 BackScreenModes = Literal["back_to_unlock", "nothing_to_back", "back_to_previous"]
 
 
-class FormScreenBase(CliveScreen, Contextual[ContextT]):
-    def __init__(self, owner: Form[ContextT]) -> None:
+class FormScreenBase(CliveScreen, Contextual[FormContextT]):
+    def __init__(self, owner: Form[FormContextT]) -> None:
         self._owner = owner
         super().__init__()
 
     @property
-    def context(self) -> ContextT:
+    def context(self) -> FormContextT:
         return self._owner.context
 
 
-class FormScreen(FormScreenBase[ContextT], ABC):
+class FormScreen(FormScreenBase[FormContextT], ABC):
     BINDINGS = [
         Binding("escape", "previous_screen", "Previous screen", show=False),
         Binding(NEXT_SCREEN_BINDING_KEY, "next_screen", "Next screen"),
-- 
GitLab