This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 0da003c  Allow custom widgets
0da003c is described below

commit 0da003cef6dbdf93b5786cbb62f644e118a3ec98
Author: Sean B. Palmer <[email protected]>
AuthorDate: Sun Nov 9 12:32:32 2025 +0000

    Allow custom widgets
---
 atr/form.py        | 16 ++++++++++++++++
 atr/get/test.py    | 12 ++++++++++++
 atr/post/test.py   |  3 ++-
 atr/shared/test.py |  1 +
 4 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/atr/form.py b/atr/form.py
index 81ad0d7..dec43a5 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -55,6 +55,7 @@ class Empty(Form):
 class Widget(enum.Enum):
     CHECKBOX = "checkbox"
     CHECKBOXES = "checkboxes"
+    CUSTOM = "custom"
     EMAIL = "email"
     FILE = "file"
     FILES = "files"
@@ -202,6 +203,7 @@ async def render(
     defaults: dict[str, Any] | None = None,
     errors: dict[str, list[str]] | None = None,
     use_error_data: bool = True,
+    custom: dict[str, htm.Element | htm.VoidElement] | None = None,
 ) -> htm.Element:
     if action is None:
         action = quart.request.path
@@ -229,6 +231,7 @@ async def render(
             defaults,
             errors,
             textarea_rows,
+            custom,
         )
         if hidden_field:
             hidden_fields.append(hidden_field)
@@ -250,6 +253,10 @@ async def render(
         submit_row = htm.div(".row")[submit_div[submit_div_contents]]
         form_children.append(submit_row)
 
+    if custom:
+        unused = ", ".join(custom.keys())
+        raise ValueError(f"Custom widgets provided but not used: {unused}")
+
     return htm.form(form_classes, action=action, method="post", 
enctype="multipart/form-data")[form_children]
 
 
@@ -378,6 +385,7 @@ def _render_widget(  # noqa: C901
     field_errors: list[str] | None,
     is_required: bool,
     textarea_rows: int,
+    custom: dict[str, htm.Element | htm.VoidElement] | None,
 ) -> htm.Element | htm.VoidElement:
     widget_type = _get_widget_type(field_info)
     widget_classes = _get_widget_classes(widget_type, field_errors)
@@ -422,6 +430,12 @@ def _render_widget(  # noqa: C901
             elements.extend(checkboxes)
             widget = htm.div[checkboxes]
 
+        case Widget.CUSTOM:
+            if custom and (field_name in custom):
+                widget = custom.pop(field_name)
+            else:
+                widget = htm.div[f"Custom widget for {field_name} not 
provided"]
+
         case Widget.EMAIL:
             attrs = {**base_attrs, "type": "email"}
             if field_value:
@@ -643,6 +657,7 @@ def _render_row(
     defaults: dict[str, Any] | None,
     errors: dict[str, list[str]] | None,
     textarea_rows: int,
+    custom: dict[str, htm.Element | htm.VoidElement] | None,
 ) -> tuple[htm.VoidElement | None, htm.Element | None]:
     widget_type = _get_widget_type(field_info)
     has_flash_error = field_name in flash_error_data
@@ -674,6 +689,7 @@ def _render_row(
         field_errors=field_errors,
         is_required=is_required,
         textarea_rows=textarea_rows,
+        custom=custom,
     )
 
     row_div = htm.div(".mb-3.row")
diff --git a/atr/get/test.py b/atr/get/test.py
index 8998d93..e24e986 100644
--- a/atr/get/test.py
+++ b/atr/get/test.py
@@ -91,10 +91,22 @@ async def test_multiple(session: web.Committer | None) -> 
str:
 
 @get.public("/test/single")
 async def test_single(session: web.Committer | None) -> str:
+    import htpy
+
+    vote_widget = htpy.div(class_="btn-group", role="group")[
+        htpy.input(type="radio", class_="btn-check", name="vote", id="vote_0", 
value="+1", autocomplete="off"),
+        htpy.label(class_="btn btn-outline-success", for_="vote_0")["+1"],
+        htpy.input(type="radio", class_="btn-check", name="vote", id="vote_1", 
value="0", autocomplete="off"),
+        htpy.label(class_="btn btn-outline-secondary", for_="vote_1")["0"],
+        htpy.input(type="radio", class_="btn-check", name="vote", id="vote_2", 
value="-1", autocomplete="off"),
+        htpy.label(class_="btn btn-outline-danger", for_="vote_2")["-1"],
+    ]
+
     single_form = await form.render(
         model_cls=shared.test.SingleForm,
         submit_label="Submit",
         action="/test/single",
+        custom={"vote": vote_widget},
     )
 
     forms_html = htm.div[
diff --git a/atr/post/test.py b/atr/post/test.py
index d5a4a01..b64a948 100644
--- a/atr/post/test.py
+++ b/atr/post/test.py
@@ -66,7 +66,8 @@ async def test_single(session: web.Committer | None, form: 
shared.test.SingleFor
         f" email={form.email},"
         f" message={form.message},"
         f" files={file_names},"
-        f" compatibility={compatibility_names}"
+        f" compatibility={compatibility_names},"
+        f" vote={form.vote}"
     )
     log.info(msg)
     await quart.flash(msg, "success")
diff --git a/atr/shared/test.py b/atr/shared/test.py
index 971f477..7fd3a7b 100644
--- a/atr/shared/test.py
+++ b/atr/shared/test.py
@@ -57,6 +57,7 @@ class SingleForm(form.Form):
     message: str = form.label("Message")
     files: form.FileList = form.label("Files to upload")
     compatibility: form.Set[Compatibility] = form.label("Compatibility")
+    vote: Literal["+1", "0", "-1"] = form.label("Vote", 
widget=form.Widget.CUSTOM)
 
     @pydantic.field_validator("email")
     @classmethod


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to