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 57efaf2  Treat select like radio widgets, and reorder interfaces
57efaf2 is described below

commit 57efaf2cd7028325e535c47ecef9baa3602512da
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Nov 13 10:26:14 2025 +0000

    Treat select like radio widgets, and reorder interfaces
---
 atr/form.py | 398 ++++++++++++++++++++++++++++++++----------------------------
 1 file changed, 212 insertions(+), 186 deletions(-)

diff --git a/atr/form.py b/atr/form.py
index a836e9c..fa715b0 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -82,6 +82,10 @@ def flash_error_data(
 
     # It is not valid Python syntax to use type[Form]() in a match branch
     if isinstance(form_cls, TypeAliasType):
+        # # Try to get the discriminator from form data first, then from errors
+        # discriminator_value = form_data.get(DISCRIMINATOR_NAME)
+        # if discriminator_value is None:
+        #     discriminator_value = _discriminator_from_errors(errors)
         discriminator_value = _discriminator_from_errors(errors)
         concrete_cls = _get_concrete_cls(form_cls, discriminator_value)
     else:
@@ -539,192 +543,6 @@ def widget(widget_type: Widget) -> Any:
     return pydantic.Field(..., json_schema_extra={"widget": widget_type.value})
 
 
-def _render_widget(  # noqa: C901
-    field_name: str,
-    field_info: pydantic.fields.FieldInfo,
-    field_value: Any,
-    field_errors: list[str] | None,
-    is_required: bool,
-    textarea_rows: int,
-    custom: dict[str, htm.Element | htm.VoidElement] | None,
-    defaults: dict[str, Any] | None,
-) -> htm.Element | htm.VoidElement:
-    widget_type = _get_widget_type(field_info)
-    widget_classes = _get_widget_classes(widget_type, field_errors)
-
-    base_attrs: dict[str, str] = {"name": field_name, "id": field_name, 
"class_": widget_classes}
-
-    elements: list[htm.Element | htm.VoidElement] = []
-
-    match widget_type:
-        case Widget.CHECKBOX:
-            attrs: dict[str, str] = {
-                "type": "checkbox",
-                "name": field_name,
-                "id": field_name,
-                "class_": "form-check-input",
-            }
-            if field_value:
-                attrs["checked"] = ""
-            widget = htpy.input(**attrs)
-
-        case Widget.CHECKBOXES:
-            choices = _get_choices(field_info)
-
-            if (not choices) and isinstance(field_value, list) and field_value:
-                # Render list[str] as checkboxes
-                if isinstance(field_value[0], tuple) and (len(field_value[0]) 
== 2):
-                    choices = field_value
-                    selected_values = []
-                else:
-                    choices = [(str(v), str(v)) for v in field_value]
-                    selected_values = field_value
-            elif isinstance(field_value, set):
-                selected_values = [item.value for item in field_value]
-            else:
-                selected_values = field_value if isinstance(field_value, list) 
else []
-
-            checkboxes = []
-            for val, label in choices:
-                checkbox_id = f"{field_name}_{val}"
-                checkbox_attrs: dict[str, str] = {
-                    "type": "checkbox",
-                    "name": field_name,
-                    "id": checkbox_id,
-                    "value": val,
-                    "class_": "form-check-input",
-                }
-                if val in selected_values:
-                    checkbox_attrs["checked"] = ""
-                checkbox_input = htpy.input(**checkbox_attrs)
-                checkbox_label = htpy.label(for_=checkbox_id, 
class_="form-check-label")[label]
-                
checkboxes.append(htpy.div(class_="form-check")[checkbox_input, checkbox_label])
-            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:
-                attrs["value"] = str(field_value)
-            widget = htpy.input(**attrs)
-
-        case Widget.FILE:
-            widget = htpy.input(type="file", **base_attrs)
-
-        case Widget.FILES:
-            attrs = {**base_attrs, "multiple": ""}
-            widget = htpy.input(type="file", **attrs)
-
-        case Widget.HIDDEN:
-            attrs = {"type": "hidden", "name": field_name, "id": field_name}
-            if field_value is not None:
-                attrs["value"] = str(field_value)
-            widget = htpy.input(**attrs)
-
-        case Widget.NUMBER:
-            attrs = {**base_attrs, "type": "number"}
-            attrs["value"] = "0" if (field_value is None) else str(field_value)
-            widget = htpy.input(**attrs)
-
-        case Widget.RADIO:
-            # We need to check the defaults because the choices might be 
dynamic
-            default_value = defaults.get(field_name) if defaults else None
-            if isinstance(default_value, list) and default_value:
-                if isinstance(default_value[0], tuple) and 
(len(default_value[0]) == 2):
-                    choices = default_value
-                    selected_value = field_value if not 
isinstance(field_value, list) else None
-                else:
-                    choices = [(val, val) for val in default_value]
-                    selected_value = (
-                        field_value
-                        if not isinstance(field_value, list)
-                        else (default_value[0] if default_value else None)
-                    )
-            elif isinstance(field_value, list) and field_value:
-                if isinstance(field_value[0], tuple) and (len(field_value[0]) 
== 2):
-                    choices = field_value
-                    selected_value = None
-                else:
-                    choices = [(val, val) for val in field_value]
-                    selected_value = field_value[0] if field_value else None
-            else:
-                choices = _get_choices(field_info)
-                selected_value = field_value
-
-            radios = []
-            for val, label in choices:
-                radio_id = f"{field_name}_{val}"
-                radio_attrs: dict[str, str] = {
-                    "type": "radio",
-                    "name": field_name,
-                    "id": radio_id,
-                    "value": val,
-                    "class_": "form-check-input",
-                }
-                if is_required:
-                    radio_attrs["required"] = ""
-                if val == selected_value:
-                    radio_attrs["checked"] = ""
-                radio_input = htpy.input(**radio_attrs)
-                radio_label = htpy.label(for_=radio_id, 
class_="form-check-label")[label]
-                radios.append(htpy.div(class_="form-check")[radio_input, 
radio_label])
-            elements.extend(radios)
-            widget = htm.div[radios]
-
-        case Widget.SELECT:
-            if isinstance(field_value, list):
-                choices = [(val, val) for val in field_value]
-                selected_value = field_value[0] if field_value else None
-            else:
-                choices = _get_choices(field_info)
-                # If field_value is an enum, extract its value for comparison
-                if isinstance(field_value, enum.Enum):
-                    selected_value = field_value.value
-                else:
-                    selected_value = field_value
-
-            options = [
-                htpy.option(
-                    value=val,
-                    selected="" if (val == selected_value) else None,
-                )[label]
-                for val, label in choices
-            ]
-            widget = htpy.select(**base_attrs)[options]
-
-        case Widget.TEXT:
-            attrs = {**base_attrs, "type": "text"}
-            if field_value:
-                attrs["value"] = str(field_value)
-            widget = htpy.input(**attrs)
-
-        case Widget.TEXTAREA:
-            attrs = {**base_attrs, "rows": str(textarea_rows)}
-            widget = htpy.textarea(**attrs)[field_value or ""]
-
-        case Widget.URL:
-            attrs = {**base_attrs, "type": "url"}
-            if field_value:
-                attrs["value"] = str(field_value)
-            widget = htpy.input(**attrs)
-
-    if not elements:
-        elements.append(widget)
-
-    if field_errors:
-        error_text = " ".join(field_errors)
-        error_div = htm.div(".invalid-feedback.d-block")[error_text]
-        elements.append(error_div)
-
-    return htm.div[elements] if len(elements) > 1 else elements[0]
-
-
 def _get_choices(field_info: pydantic.fields.FieldInfo) -> list[tuple[str, 
str]]:  # noqa: C901
     annotation = field_info.annotation
     origin = get_origin(annotation)
@@ -852,6 +670,42 @@ def _get_widget_type(field_info: 
pydantic.fields.FieldInfo) -> Widget:  # noqa:
     return Widget.TEXT
 
 
+def _parse_dynamic_choices(
+    field_name: str, defaults: dict[str, Any] | None, field_value: Any
+) -> tuple[list[tuple[str, str]], Any]:
+    """Parse dynamic choices from defaults or field value."""
+    # Check defaults first for dynamic choices
+    default_value = defaults.get(field_name) if defaults else None
+
+    if isinstance(default_value, list) and default_value:
+        if isinstance(default_value[0], tuple) and (len(default_value[0]) == 
2):
+            # List of (value, label) tuples
+            choices = default_value
+            selected_value = field_value if not isinstance(field_value, list) 
else None
+            return choices, selected_value
+        else:
+            # List of simple values
+            choices = [(val, val) for val in default_value]
+            selected_value = (
+                field_value if not isinstance(field_value, list) else 
(default_value[0] if default_value else None)
+            )
+            return choices, selected_value
+
+    # Otherwise use field_value, if it's a list
+    if isinstance(field_value, list) and field_value:
+        if isinstance(field_value[0], tuple) and (len(field_value[0]) == 2):
+            choices = field_value
+            selected_value = None
+            return choices, selected_value
+        else:
+            choices = [(val, val) for val in field_value]
+            selected_value = field_value[0] if field_value else None
+            return choices, selected_value
+
+    # Otherwise there were no dynamic choices
+    return [], field_value
+
+
 def _render_field_value(
     field_name: str,
     flash_error_data: dict[str, Any],
@@ -951,3 +805,175 @@ def _render_row(
                     widget_div_contents.append(doc_div)
 
     return None, row_div[label_elem, widget_div[widget_div_contents]]
+
+
+def _render_widget(  # noqa: C901
+    field_name: str,
+    field_info: pydantic.fields.FieldInfo,
+    field_value: Any,
+    field_errors: list[str] | None,
+    is_required: bool,
+    textarea_rows: int,
+    custom: dict[str, htm.Element | htm.VoidElement] | None,
+    defaults: dict[str, Any] | None,
+) -> htm.Element | htm.VoidElement:
+    widget_type = _get_widget_type(field_info)
+    widget_classes = _get_widget_classes(widget_type, field_errors)
+
+    base_attrs: dict[str, str] = {"name": field_name, "id": field_name, 
"class_": widget_classes}
+
+    elements: list[htm.Element | htm.VoidElement] = []
+
+    match widget_type:
+        case Widget.CHECKBOX:
+            attrs: dict[str, str] = {
+                "type": "checkbox",
+                "name": field_name,
+                "id": field_name,
+                "class_": "form-check-input",
+            }
+            if field_value:
+                attrs["checked"] = ""
+            widget = htpy.input(**attrs)
+
+        case Widget.CHECKBOXES:
+            choices = _get_choices(field_info)
+
+            if (not choices) and isinstance(field_value, list) and field_value:
+                # Render list[str] as checkboxes
+                if isinstance(field_value[0], tuple) and (len(field_value[0]) 
== 2):
+                    choices = field_value
+                    selected_values = []
+                else:
+                    choices = [(str(v), str(v)) for v in field_value]
+                    selected_values = field_value
+            elif isinstance(field_value, set):
+                selected_values = [item.value for item in field_value]
+            else:
+                selected_values = field_value if isinstance(field_value, list) 
else []
+
+            checkboxes = []
+            for val, label in choices:
+                checkbox_id = f"{field_name}_{val}"
+                checkbox_attrs: dict[str, str] = {
+                    "type": "checkbox",
+                    "name": field_name,
+                    "id": checkbox_id,
+                    "value": val,
+                    "class_": "form-check-input",
+                }
+                if val in selected_values:
+                    checkbox_attrs["checked"] = ""
+                checkbox_input = htpy.input(**checkbox_attrs)
+                checkbox_label = htpy.label(for_=checkbox_id, 
class_="form-check-label")[label]
+                
checkboxes.append(htpy.div(class_="form-check")[checkbox_input, checkbox_label])
+            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:
+                attrs["value"] = str(field_value)
+            widget = htpy.input(**attrs)
+
+        case Widget.FILE:
+            widget = htpy.input(type="file", **base_attrs)
+
+        case Widget.FILES:
+            attrs = {**base_attrs, "multiple": ""}
+            widget = htpy.input(type="file", **attrs)
+
+        case Widget.HIDDEN:
+            attrs = {"type": "hidden", "name": field_name, "id": field_name}
+            if field_value is not None:
+                attrs["value"] = str(field_value)
+            widget = htpy.input(**attrs)
+
+        case Widget.NUMBER:
+            attrs = {**base_attrs, "type": "number"}
+            attrs["value"] = "0" if (field_value is None) else str(field_value)
+            widget = htpy.input(**attrs)
+
+        case Widget.RADIO:
+            # Check for dynamic choices from defaults or field_value
+            dynamic_choices, selected_value = 
_parse_dynamic_choices(field_name, defaults, field_value)
+            if dynamic_choices:
+                choices = dynamic_choices
+            else:
+                choices = _get_choices(field_info)
+                selected_value = field_value
+
+            radios = []
+            for val, label in choices:
+                radio_id = f"{field_name}_{val}"
+                radio_attrs: dict[str, str] = {
+                    "type": "radio",
+                    "name": field_name,
+                    "id": radio_id,
+                    "value": val,
+                    "class_": "form-check-input",
+                }
+                if is_required:
+                    radio_attrs["required"] = ""
+                if val == selected_value:
+                    radio_attrs["checked"] = ""
+                radio_input = htpy.input(**radio_attrs)
+                radio_label = htpy.label(for_=radio_id, 
class_="form-check-label")[label]
+                radios.append(htpy.div(class_="form-check")[radio_input, 
radio_label])
+            elements.extend(radios)
+            widget = htm.div[radios]
+
+        case Widget.SELECT:
+            # Check for dynamic choices from defaults or field_value
+            dynamic_choices, selected_value = 
_parse_dynamic_choices(field_name, defaults, field_value)
+
+            if dynamic_choices:
+                choices = dynamic_choices
+            else:
+                choices = _get_choices(field_info)
+                # If field_value is an enum, extract its value for comparison
+                if isinstance(field_value, enum.Enum):
+                    selected_value = field_value.value
+                else:
+                    selected_value = field_value
+
+            options = [
+                htpy.option(
+                    value=val,
+                    selected="" if (val == selected_value) else None,
+                )[label]
+                for val, label in choices
+            ]
+            widget = htpy.select(**base_attrs)[options]
+
+        case Widget.TEXT:
+            attrs = {**base_attrs, "type": "text"}
+            if field_value:
+                attrs["value"] = str(field_value)
+            widget = htpy.input(**attrs)
+
+        case Widget.TEXTAREA:
+            attrs = {**base_attrs, "rows": str(textarea_rows)}
+            widget = htpy.textarea(**attrs)[field_value or ""]
+
+        case Widget.URL:
+            attrs = {**base_attrs, "type": "url"}
+            if field_value:
+                attrs["value"] = str(field_value)
+            widget = htpy.input(**attrs)
+
+    if not elements:
+        elements.append(widget)
+
+    if field_errors:
+        error_text = " ".join(field_errors)
+        error_div = htm.div(".invalid-feedback.d-block")[error_text]
+        elements.append(error_div)
+
+    return htm.div[elements] if len(elements) > 1 else elements[0]


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

Reply via email to