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 38ec5d7  Fix the file form type and add an optional URL type
38ec5d7 is described below

commit 38ec5d7e3c01d8881a64ab97f6d4cfd1ada4adcb
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Nov 12 18:31:39 2025 +0000

    Fix the file form type and add an optional URL type
---
 atr/form.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 76 insertions(+), 22 deletions(-)

diff --git a/atr/form.py b/atr/form.py
index d7ed67b..280e5d6 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -248,6 +248,7 @@ def render(  # noqa: C901
     empty: bool = False,
     border: bool = False,
     wider_widgets: bool = False,
+    skip: list[str] | None = None,
 ) -> htm.Element:
     if action is None:
         action = quart.request.path
@@ -262,15 +263,16 @@ def render(  # noqa: C901
         form_classes += ".px-5"
 
     flash_error_data: dict[str, Any] = _get_flash_error_data() if 
use_error_data else {}
-
     field_rows: list[htm.Element] = []
     hidden_fields: list[htm.Element | htm.VoidElement | markupsafe.Markup] = []
-
     hidden_fields.append(csrf_input())
+    skip_fields = set(skip) if skip else set()
 
     for field_name, field_info in model_cls.model_fields.items():
         if field_name == "csrf_token":
             continue
+        if field_name in skip_fields:
+            continue
 
         hidden_field, row = _render_row(
             field_info,
@@ -344,10 +346,25 @@ def to_enum_set[EnumType: enum.Enum](v: Any, enum_class: 
type[EnumType]) -> set[
     raise ValueError(f"Expected a set of enum values, got {type(v).__name__}")
 
 
-def to_filestorage(v: Any) -> datastructures.FileStorage:
-    if not isinstance(v, datastructures.FileStorage):
+def to_filestorage(v: Any) -> datastructures.FileStorage | None:
+    if (v is None) or (v == ""):
+        return None
+
+    if not isinstance(v, list):
+        raise ValueError("Expected a list of uploaded files")
+    if not v:
+        return None
+    if len(v) != 1:
+        raise ValueError("Expected a single uploaded file")
+
+    fs = v[0]
+    if not isinstance(fs, datastructures.FileStorage):
         raise ValueError("Expected an uploaded file")
-    return v
+
+    if not fs.filename:
+        return None
+
+    return fs
 
 
 def to_filestorage_list(v: Any) -> list[datastructures.FileStorage]:
@@ -394,6 +411,12 @@ def to_int(v: Any) -> int:
         raise ValueError(f"Invalid integer value: {v!r}")
 
 
+def to_optional_url(v: Any) -> pydantic.HttpUrl | None:
+    if (v is None) or (v == ""):
+        return None
+    return pydantic.TypeAdapter(pydantic.HttpUrl).validate_python(v)
+
+
 def to_str_list(v: Any) -> list[str]:
     # TODO: Might need to handle the empty case
     if isinstance(v, list):
@@ -414,11 +437,11 @@ Bool = Annotated[
 
 Email = pydantic.EmailStr
 
-URL = pydantic.HttpUrl
 
 File = Annotated[
-    datastructures.FileStorage,
+    datastructures.FileStorage | None,
     functional_validators.BeforeValidator(to_filestorage),
+    pydantic.Field(default=None),
 ]
 
 FileList = Annotated[
@@ -427,24 +450,33 @@ FileList = Annotated[
     pydantic.Field(default_factory=list),
 ]
 
-StrList = Annotated[
-    list[str],
-    functional_validators.BeforeValidator(to_str_list),
-    pydantic.Field(default_factory=list),
-]
-
 Filename = Annotated[
     pathlib.Path | None,
     functional_validators.BeforeValidator(to_filename),
     pydantic.Field(default=None),
 ]
 
+OptionalURL = Annotated[
+    pydantic.HttpUrl | None,
+    functional_validators.BeforeValidator(to_optional_url),
+    pydantic.Field(default=None),
+]
+
+StrList = Annotated[
+    list[str],
+    functional_validators.BeforeValidator(to_str_list),
+    pydantic.Field(default_factory=list),
+]
+
 Int = Annotated[
     int,
     functional_validators.BeforeValidator(to_int),
 ]
 
 
+URL = pydantic.HttpUrl
+
+
 class Set[EnumType: enum.Enum]:
     def __iter__(self) -> Iterator[EnumType]:
         # For type checkers
@@ -489,6 +521,7 @@ def _render_widget(  # noqa: C901
     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)
@@ -574,9 +607,26 @@ def _render_widget(  # noqa: C901
             widget = htpy.input(**attrs)
 
         case Widget.RADIO:
-            if isinstance(field_value, list):
-                choices = [(val, val) for val in field_value]
-                selected_value = field_value[0] if field_value else None
+            # 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
@@ -832,6 +882,7 @@ def _render_row(
         is_required=is_required,
         textarea_rows=textarea_rows,
         custom=custom,
+        defaults=defaults,
     )
 
     row_div = htm.div(f".mb-3.pb-3.row{'.border-bottom' if border else ''}")
@@ -843,11 +894,14 @@ def _render_row(
         error_div = htm.div(".text-danger.mt-1")[f"Error: {error_msg}"]
         widget_div_contents.append(error_div)
     else:
-        json_schema_extra = field_info.json_schema_extra or {}
-        if isinstance(json_schema_extra, dict):
-            documentation = json_schema_extra.get("documentation")
-            if isinstance(documentation, str):
-                doc_div = htm.div(".text-muted.mt-1.form-text")[documentation]
-                widget_div_contents.append(doc_div)
+        # Skip documentation for CUSTOM widgets
+        # Therefore CUSTOM widgets must handle their own documentation
+        if widget_type != Widget.CUSTOM:
+            json_schema_extra = field_info.json_schema_extra or {}
+            if isinstance(json_schema_extra, dict):
+                documentation = json_schema_extra.get("documentation")
+                if isinstance(documentation, str):
+                    doc_div = 
htm.div(".text-muted.mt-1.form-text")[documentation]
+                    widget_div_contents.append(doc_div)
 
     return None, row_div[label_elem, widget_div[widget_div_contents]]


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

Reply via email to