Repository: marimo-team/marimo
Pull request: Add validation for file type extensions in mo.ui.file
Add validation for file type extensions in mo.ui.file

This PR adds validation to ensure file types in `mo.ui.file()` have leading dots, providing a clear error message when they don't. This fixes the cryptic error message users were seeing when using file types without dots.

Changes:
- Add validation in `file` class to ensure file types have leading dots
- Add clear error message explaining the requirement (e.g., '.csv' instead of 'csv')
- Handle special media types (audio/*, video/*, image/*)
- Add comprehensive tests for file type validation

Testing:
- Added new test case `test_file_validation` that verifies:
  - Valid file types (with dots) are accepted
  - Invalid file types raise appropriate error
  - Special media types are handled correctly
  - Mixed valid types are allowed
- All tests passing

Fixes #3065

Link to Devin run: https://app.devin.ai/sessions/49c78b1c7dc14f818140c34bb51b283c


Problem statement:
cryptic error message on mo.ui.file upload failure
### Describe the bug

When uploading a file using the call `mo.ui.file(filetypes=['csv'],kind='area')` you get the following error popup in the low right corner:

>  **File Upload Failed**
>  FILENAME (file type must be text/plain)

This has nothing obvious to do with the root cause, which is that the filetype must have a leading dot, i.e., `mo.ui.file(filetypes=['.csv'],kind='area')`.  Needless to say this is rather difficult to figure out.

### Environment

wasm

### Code to reproduce

import marimo as mo
mo.ui.file(filetypes=['csv'],kind='area')

Hints from issue discussion:

Commit message: [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

Patch to review:
diff --git a/marimo/_plugins/ui/_impl/input.py b/marimo/_plugins/ui/_impl/input.py
index 3bc0521f996..890c582e69d 100644
--- a/marimo/_plugins/ui/_impl/input.py
+++ b/marimo/_plugins/ui/_impl/input.py
@@ -1308,6 +1308,22 @@ def __init__(
             Callable[[Sequence[FileUploadResults]], None]
         ] = None,
     ) -> None:
+        # Validate filetypes have leading dots
+        if filetypes is not None:
+            invalid_types = [
+                ft
+                for ft in filetypes
+                if not (
+                    ft.startswith(".")
+                    or ft in ("audio/*", "video/*", "image/*")
+                )
+            ]
+            if invalid_types:
+                raise ValueError(
+                    f"File types must start with a dot (e.g., '.csv' instead of 'csv'). "
+                    f"Invalid types: {', '.join(invalid_types)}"
+                )
+
         super().__init__(
             component_name=file._name,
             initial_value=[],
diff --git a/tests/_plugins/ui/_impl/test_input.py b/tests/_plugins/ui/_impl/test_input.py
index 64ed4ee2a37..e2dee66e3ad 100644
--- a/tests/_plugins/ui/_impl/test_input.py
+++ b/tests/_plugins/ui/_impl/test_input.py
@@ -460,7 +460,30 @@ def test_form_in_dictionary_allowed() -> None:
     assert checkbox._id != d["form"].element._id
 
 
-# TODO(akshayka): test file
+def test_file_validation() -> None:
+    """Test file type validation in the file class."""
+    # Valid filetypes should be accepted
+    ui.file(filetypes=[".csv", ".txt"])
+    ui.file(filetypes=["audio/*"])
+    ui.file(filetypes=["video/*"])
+    ui.file(filetypes=["image/*"])
+    ui.file(filetypes=[".csv", "audio/*"])  # Mixed types are allowed
+
+    # Invalid filetypes should raise ValueError
+    with pytest.raises(ValueError) as e:
+        ui.file(filetypes=["csv"])
+    assert "must start with a dot" in str(e.value)
+    assert "csv" in str(e.value)
+
+    with pytest.raises(ValueError) as e:
+        ui.file(filetypes=["txt", ".csv"])
+    assert "must start with a dot" in str(e.value)
+    assert "txt" in str(e.value)
+
+    with pytest.raises(ValueError) as e:
+        ui.file(filetypes=["doc", "pdf"])
+    assert "must start with a dot" in str(e.value)
+    assert "doc, pdf" in str(e.value)
 
 
 @pytest.mark.skipif(not HAS_NUMPY, reason="numpy not installed")


Code review comments:
diff_hunk: @@ -1308,6 +1308,22 @@ def __init__(
             Callable[[Sequence[FileUploadResults]], None]
         ] = None,
     ) -> None:
+        # Validate filetypes have leading dots
+        if filetypes is not None:
+            invalid_types = [
+                ft
+                for ft in filetypes
+                if not (
+                    ft.startswith(".")
+                    or ft in ("audio/*", "video/*", "image/*")
path: marimo/_plugins/ui/_impl/input.py
text: @user1:
we should allow more than these file types. just check if there is a `/` instead