Skip to content

[feature] Add st.file_uploader support to AppTest#14341

Merged
lukasmasuch merged 2 commits intodevelopfrom
lukasmasuch/apptest-file-uploader
Mar 12, 2026
Merged

[feature] Add st.file_uploader support to AppTest#14341
lukasmasuch merged 2 commits intodevelopfrom
lukasmasuch/apptest-file-uploader

Conversation

@lukasmasuch
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch commented Mar 11, 2026

Describe your changes

Adds st.file_uploader support to the AppTest testing framework, addressing a long-standing feature request.

  • Add FileUploader widget class to element_tree.py with set_value(), upload(), and clear() methods
  • Add file registration logic to app_test.py that registers uploaded files with MemoryUploadedFileManager before script runs
  • Add comprehensive unit tests covering single/multiple file uploads, incremental uploads, and clearing

Example usage:

def test_file_upload():
    at = AppTest.from_file("app.py")
    at.run()
    
    # Upload a file
    at.file_uploader[0].set_value(("data.csv", b"col1,col2\n1,2", "text/csv"))
    at.run()
    
    # Or upload multiple files
    at.file_uploader[0].set_value([
        ("file1.txt", b"content1", "text/plain"),
        ("file2.txt", b"content2", "text/plain"),
    ])
    at.run()

GitHub Issue Link (if applicable)

Testing Plan

  • Unit Tests (Python)
    • Test single file upload
    • Test multiple file upload
    • Test incremental upload via upload() method
    • Test clearing files via clear() method
    • Test key-based widget access
Agent metrics
Type Name Count
skill checking-changes 2
skill finalizing-pr 1
skill updating-internal-docs 1
subagent Explore 2
subagent fixing-pr 1
subagent general-purpose 3
subagent reviewing-local-changes 1
subagent simplifying-local-changes 1

Add FileUploader widget class to AppTest framework enabling file upload
testing in Streamlit applications. Files are registered with the
MemoryUploadedFileManager before script runs using pre-generated UUIDs.

Closes #8093

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lukasmasuch lukasmasuch added change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users labels Mar 11, 2026
Copilot AI review requested due to automatic review settings March 11, 2026 22:37
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 11, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14341/streamlit-1.55.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14341.streamlit.app (☁️ Deploy here if not accessible)

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds st.file_uploader support to the st.testing.v1.AppTest framework, enabling tests to programmatically upload/clear files and have apps under test consume them via Streamlit’s uploaded-file machinery.

Changes:

  • Introduces a FileUploader widget node in the testing element tree with set_value(), upload(), and clear() APIs.
  • Registers uploaded file bytes into the runner’s MemoryUploadedFileManager prior to each script run.
  • Adds unit tests for single/multiple uploads, incremental uploads, clearing, and key-based access.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
lib/tests/streamlit/testing/element_tree_test.py Adds coverage for AppTest.file_uploader interactions and basic behavior.
lib/streamlit/testing/v1/element_tree.py Adds FileUploader widget representation and parsing/accessors in the element tree.
lib/streamlit/testing/v1/app_test.py Adds pre-run file registration so uploaded files are available during script execution.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 11, 2026

📈 Python coverage change detected

The Python unit test coverage has increased by 0.0439%

  • Current PR: 93.5483% (24195 statements, 1561 missed)
  • Latest develop: 93.5043% (24093 statements, 1565 missed)

✅ Coverage change is within normal range.

Coverage by files
Name Stmts Miss Cover
streamlit/__init__.py 140 0 100%
streamlit/__main__.py 3 3 0%
streamlit/auth_util.py 216 11 95%
streamlit/cli_util.py 33 0 100%
streamlit/column_config.py 3 0 100%
streamlit/commands/__init__.py 0 0 100%
streamlit/commands/echo.py 54 2 96%
streamlit/commands/execution_control.py 70 10 86%
streamlit/commands/logo.py 53 1 98%
streamlit/commands/navigation.py 128 2 98%
streamlit/commands/page_config.py 106 4 96%
streamlit/components/__init__.py 0 0 100%
streamlit/components/lib/__init__.py 0 0 100%
streamlit/components/lib/local_component_registry.py 35 2 94%
streamlit/components/types/__init__.py 0 0 100%
streamlit/components/types/base_component_registry.py 14 0 100%
streamlit/components/types/base_custom_component.py 48 6 88%
streamlit/components/v1/__init__.py 5 0 100%
streamlit/components/v1/component_arrow.py 33 2 94%
streamlit/components/v1/component_registry.py 35 3 91%
streamlit/components/v1/components.py 4 4 0%
streamlit/components/v1/custom_component.py 82 5 94%
streamlit/components/v2/__init__.py 27 0 100%
streamlit/components/v2/bidi_component/__init__.py 4 0 100%
streamlit/components/v2/bidi_component/constants.py 5 0 100%
streamlit/components/v2/bidi_component/main.py 148 17 89%
streamlit/components/v2/bidi_component/serialization.py 81 2 98%
streamlit/components/v2/bidi_component/state.py 14 0 100%
streamlit/components/v2/component_definition_resolver.py 30 0 100%
streamlit/components/v2/component_file_watcher.py 117 9 92%
streamlit/components/v2/component_manager.py 97 13 87%
streamlit/components/v2/component_manifest_handler.py 24 0 100%
streamlit/components/v2/component_path_utils.py 65 5 92%
streamlit/components/v2/component_registry.py 121 8 93%
streamlit/components/v2/get_bidi_component_manager.py 8 1 88%
streamlit/components/v2/manifest_scanner.py 227 25 89%
streamlit/components/v2/presentation.py 84 19 77%
streamlit/components/v2/types.py 8 8 0%
streamlit/config.py 420 11 97%
streamlit/config_option.py 77 3 96%
streamlit/config_util.py 308 7 98%
streamlit/connections/__init__.py 6 0 100%
streamlit/connections/base_connection.py 51 0 100%
streamlit/connections/snowflake_connection.py 98 16 84%
streamlit/connections/snowpark_connection.py 46 3 93%
streamlit/connections/sql_connection.py 56 6 89%
streamlit/connections/util.py 33 0 100%
streamlit/cursor.py 130 1 99%
streamlit/dataframe_util.py 497 40 92%
streamlit/delta_generator.py 253 7 97%
streamlit/delta_generator_singletons.py 86 7 92%
streamlit/deprecation_util.py 66 4 94%
streamlit/development.py 1 0 100%
streamlit/elements/__init__.py 0 0 100%
streamlit/elements/alert.py 72 0 100%
streamlit/elements/arrow.py 222 23 90%
streamlit/elements/balloons.py 10 0 100%
streamlit/elements/bokeh_chart.py 9 0 100%
streamlit/elements/code.py 20 1 95%
streamlit/elements/deck_gl_json_chart.py 104 7 93%
streamlit/elements/dialog_decorator.py 38 0 100%
streamlit/elements/empty.py 16 4 75%
streamlit/elements/exception.py 101 10 90%
streamlit/elements/form.py 56 2 96%
streamlit/elements/graphviz_chart.py 36 1 97%
streamlit/elements/heading.py 56 0 100%
streamlit/elements/help.py 227 9 96%
streamlit/elements/html.py 49 0 100%
streamlit/elements/iframe.py 29 0 100%
streamlit/elements/image.py 36 0 100%
streamlit/elements/json.py 48 6 88%
streamlit/elements/layouts.py 232 5 98%
streamlit/elements/lib/__init__.py 0 0 100%
streamlit/elements/lib/built_in_chart_utils.py 401 29 93%
streamlit/elements/lib/color_util.py 103 4 96%
streamlit/elements/lib/column_config_utils.py 169 1 99%
streamlit/elements/lib/column_types.py 200 4 98%
streamlit/elements/lib/dialog.py 67 0 100%
streamlit/elements/lib/dicttools.py 39 2 95%
streamlit/elements/lib/file_uploader_utils.py 54 0 100%
streamlit/elements/lib/form_utils.py 26 0 100%
streamlit/elements/lib/image_utils.py 176 21 88%
streamlit/elements/lib/js_number.py 28 3 89%
streamlit/elements/lib/layout_utils.py 121 1 99%
streamlit/elements/lib/mutable_expander_container.py 19 0 100%
streamlit/elements/lib/mutable_popover_container.py 19 0 100%
streamlit/elements/lib/mutable_status_container.py 71 3 96%
streamlit/elements/lib/mutable_tab_container.py 19 0 100%
streamlit/elements/lib/options_selector_utils.py 142 2 99%
streamlit/elements/lib/pandas_styler_utils.py 80 2 98%
streamlit/elements/lib/policies.py 58 1 98%
streamlit/elements/lib/shortcut_utils.py 42 2 95%
streamlit/elements/lib/streamlit_plotly_theme.py 48 0 100%
streamlit/elements/lib/subtitle_utils.py 76 5 93%
streamlit/elements/lib/utils.py 76 5 93%
streamlit/elements/map.py 110 1 99%
streamlit/elements/markdown.py 66 2 97%
streamlit/elements/media.py 181 8 96%
streamlit/elements/metric.py 102 0 100%
streamlit/elements/pdf.py 47 0 100%
streamlit/elements/plotly_chart.py 129 6 95%
streamlit/elements/progress.py 36 0 100%
streamlit/elements/pyplot.py 37 0 100%
streamlit/elements/snow.py 10 0 100%
streamlit/elements/space.py 12 0 100%
streamlit/elements/spinner.py 44 3 93%
streamlit/elements/table.py 56 3 95%
streamlit/elements/text.py 16 0 100%
streamlit/elements/toast.py 26 0 100%
streamlit/elements/vega_charts.py 244 5 98%
streamlit/elements/widgets/__init__.py 0 0 100%
streamlit/elements/widgets/audio_input.py 68 1 99%
streamlit/elements/widgets/button.py 265 6 98%
streamlit/elements/widgets/button_group.py 202 15 93%
streamlit/elements/widgets/camera_input.py 62 1 98%
streamlit/elements/widgets/chat.py 230 36 84%
streamlit/elements/widgets/checkbox.py 54 0 100%
streamlit/elements/widgets/color_picker.py 70 2 97%
streamlit/elements/widgets/data_editor.py 254 14 94%
streamlit/elements/widgets/feedback.py 69 0 100%
streamlit/elements/widgets/file_uploader.py 108 1 99%
streamlit/elements/widgets/menu_button.py 81 0 100%
streamlit/elements/widgets/multiselect.py 118 5 96%
streamlit/elements/widgets/number_input.py 153 4 97%
streamlit/elements/widgets/radio.py 105 5 95%
streamlit/elements/widgets/select_slider.py 127 4 97%
streamlit/elements/widgets/selectbox.py 99 3 97%
streamlit/elements/widgets/slider.py 256 12 95%
streamlit/elements/widgets/text_widgets.py 144 6 96%
streamlit/elements/widgets/time_widgets.py 465 25 95%
streamlit/elements/write.py 166 20 88%
streamlit/emojis.py 4 0 100%
streamlit/env_util.py 21 3 86%
streamlit/error_util.py 33 2 94%
streamlit/errors.py 193 25 87%
streamlit/external/__init__.py 0 0 100%
streamlit/external/langchain/__init__.py 2 0 100%
streamlit/external/langchain/streamlit_callback_handler.py 137 78 43%
streamlit/file_util.py 84 8 90%
streamlit/git_util.py 100 5 95%
streamlit/logger.py 54 0 100%
streamlit/material_icon_names.py 1 0 100%
streamlit/navigation/__init__.py 0 0 100%
streamlit/navigation/page.py 125 2 98%
streamlit/net_util.py 55 3 95%
streamlit/path_security.py 17 1 94%
streamlit/platform.py 10 1 90%
streamlit/runtime/__init__.py 8 0 100%
streamlit/runtime/app_session.py 466 89 81%
streamlit/runtime/caching/__init__.py 21 0 100%
streamlit/runtime/caching/cache_data_api.py 191 3 98%
streamlit/runtime/caching/cache_errors.py 44 4 91%
streamlit/runtime/caching/cache_resource_api.py 165 1 99%
streamlit/runtime/caching/cache_type.py 11 1 91%
streamlit/runtime/caching/cache_utils.py 173 6 97%
streamlit/runtime/caching/cached_message_replay.py 108 1 99%
streamlit/runtime/caching/hashing.py 310 25 92%
streamlit/runtime/caching/legacy_cache_api.py 14 0 100%
streamlit/runtime/caching/storage/__init__.py 2 0 100%
streamlit/runtime/caching/storage/cache_storage_protocol.py 29 0 100%
streamlit/runtime/caching/storage/dummy_cache_storage.py 21 0 100%
streamlit/runtime/caching/storage/in_memory_cache_storage_wrapper.py 67 1 99%
streamlit/runtime/caching/storage/local_disk_cache_storage.py 86 4 95%
streamlit/runtime/caching/ttl_cleanup_cache.py 35 0 100%
streamlit/runtime/connection_factory.py 96 11 89%
streamlit/runtime/context.py 137 0 100%
streamlit/runtime/context_util.py 18 0 100%
streamlit/runtime/credentials.py 139 4 97%
streamlit/runtime/download_data_util.py 27 0 100%
streamlit/runtime/forward_msg_cache.py 23 2 91%
streamlit/runtime/forward_msg_queue.py 63 4 94%
streamlit/runtime/fragment.py 110 1 99%
streamlit/runtime/media_file_manager.py 110 7 94%
streamlit/runtime/media_file_storage.py 15 0 100%
streamlit/runtime/memory_media_file_storage.py 73 0 100%
streamlit/runtime/memory_session_storage.py 15 0 100%
streamlit/runtime/memory_uploaded_file_manager.py 46 1 98%
streamlit/runtime/metrics_util.py 195 13 93%
streamlit/runtime/pages_manager.py 59 2 97%
streamlit/runtime/runtime.py 250 16 94%
streamlit/runtime/runtime_util.py 30 1 97%
streamlit/runtime/script_data.py 16 0 100%
streamlit/runtime/scriptrunner/__init__.py 5 0 100%
streamlit/runtime/scriptrunner/exec_code.py 49 5 90%
streamlit/runtime/scriptrunner/magic.py 83 1 99%
streamlit/runtime/scriptrunner/magic_funcs.py 10 1 90%
streamlit/runtime/scriptrunner/script_cache.py 27 0 100%
streamlit/runtime/scriptrunner/script_runner.py 231 25 89%
streamlit/runtime/scriptrunner_utils/__init__.py 0 0 100%
streamlit/runtime/scriptrunner_utils/exceptions.py 9 1 89%
streamlit/runtime/scriptrunner_utils/script_requests.py 104 5 95%
streamlit/runtime/scriptrunner_utils/script_run_context.py 118 0 100%
streamlit/runtime/secrets.py 241 24 90%
streamlit/runtime/session_manager.py 71 2 97%
streamlit/runtime/state/__init__.py 7 0 100%
streamlit/runtime/state/common.py 60 1 98%
streamlit/runtime/state/presentation.py 19 4 79%
streamlit/runtime/state/query_params.py 332 6 98%
streamlit/runtime/state/query_params_proxy.py 71 0 100%
streamlit/runtime/state/safe_session_state.py 77 9 88%
streamlit/runtime/state/session_state.py 519 34 93%
streamlit/runtime/state/session_state_proxy.py 62 8 87%
streamlit/runtime/state/widgets.py 23 0 100%
streamlit/runtime/stats.py 132 4 97%
streamlit/runtime/theme_util.py 46 1 98%
streamlit/runtime/uploaded_file_manager.py 39 2 95%
streamlit/runtime/websocket_session_manager.py 112 0 100%
streamlit/source_util.py 34 1 97%
streamlit/starlette.py 2 0 100%
streamlit/string_util.py 106 9 92%
streamlit/temporary_directory.py 18 1 94%
streamlit/testing/__init__.py 0 0 100%
streamlit/testing/v1/__init__.py 2 0 100%
streamlit/testing/v1/app_test.py 259 4 98%
streamlit/testing/v1/element_tree.py 1569 70 96%
streamlit/testing/v1/local_script_runner.py 75 2 97%
streamlit/testing/v1/util.py 17 0 100%
streamlit/time_util.py 27 0 100%
streamlit/type_util.py 145 13 91%
streamlit/url_util.py 39 4 90%
streamlit/user_info.py 105 8 92%
streamlit/util.py 38 1 97%
streamlit/version.py 3 0 100%
streamlit/watcher/__init__.py 3 0 100%
streamlit/watcher/event_based_path_watcher.py 184 25 86%
streamlit/watcher/folder_black_list.py 14 1 93%
streamlit/watcher/local_sources_watcher.py 127 9 93%
streamlit/watcher/path_watcher.py 40 1 98%
streamlit/watcher/polling_path_watcher.py 55 2 96%
streamlit/watcher/util.py 59 1 98%
streamlit/web/__init__.py 0 0 100%
streamlit/web/bootstrap.py 176 21 88%
streamlit/web/cache_storage_manager_config.py 5 0 100%
streamlit/web/cli.py 184 12 93%
streamlit/web/server/__init__.py 5 0 100%
streamlit/web/server/app_discovery.py 104 5 95%
streamlit/web/server/app_static_file_handler.py 34 3 91%
streamlit/web/server/authlib_tornado_integration.py 42 5 88%
streamlit/web/server/bidi_component_request_handler.py 65 8 88%
streamlit/web/server/browser_websocket_handler.py 147 20 86%
streamlit/web/server/component_file_utils.py 27 0 100%
streamlit/web/server/component_request_handler.py 55 4 93%
streamlit/web/server/media_file_handler.py 65 9 86%
streamlit/web/server/oauth_authlib_routes.py 162 35 78%
streamlit/web/server/oidc_mixin.py 46 0 100%
streamlit/web/server/routes.py 94 10 89%
streamlit/web/server/server.py 195 13 93%
streamlit/web/server/server_util.py 87 5 94%
streamlit/web/server/starlette/__init__.py 3 0 100%
streamlit/web/server/starlette/starlette_app.py 148 4 97%
streamlit/web/server/starlette/starlette_app_utils.py 101 7 93%
streamlit/web/server/starlette/starlette_auth_routes.py 233 51 78%
streamlit/web/server/starlette/starlette_gzip_middleware.py 30 0 100%
streamlit/web/server/starlette/starlette_path_security_middleware.py 22 0 100%
streamlit/web/server/starlette/starlette_routes.py 341 82 76%
streamlit/web/server/starlette/starlette_server.py 167 7 96%
streamlit/web/server/starlette/starlette_server_config.py 13 0 100%
streamlit/web/server/starlette/starlette_static_routes.py 70 6 91%
streamlit/web/server/starlette/starlette_websocket.py 203 23 89%
streamlit/web/server/stats_request_handler.py 59 5 92%
streamlit/web/server/upload_file_request_handler.py 59 7 88%
streamlit/web/server/websocket_headers.py 19 1 95%
TOTAL 24195 1561 94%

📊 View detailed coverage comparison

@lukasmasuch lukasmasuch changed the title feat(testing): add st.file_uploader support to AppTest [feature] Add st.file_uploader support to AppTest Mar 11, 2026
Address review comments about FileUploader widget state persistence:

- Use InitialValue sentinel pattern to distinguish "not set" from "cleared"
- Fall back to existing UploadedFile objects in session_state when no
  explicit set_value/upload/clear was called, similar to other widgets
- Add register_file() helper method to LocalScriptRunner to avoid
  accessing private attributes from AppTest
- Add tests for file persistence across multiple run() calls

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lukasmasuch lukasmasuch added the ai-review If applied to PR or issue will run AI review workflow label Mar 11, 2026
@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Summary

This PR adds st.file_uploader support to the AppTest testing framework, closing a long-standing feature request (#8093). The implementation introduces a FileUploader widget class in element_tree.py with set_value(), upload(), and clear() methods, file registration logic via MemoryUploadedFileManager in app_test.py, a register_file() helper on LocalScriptRunner, and comprehensive unit tests covering single/multiple uploads, incremental uploads, clearing, key-based access, and persistence across runs.

Code Quality

The implementation is well-structured and follows existing codebase patterns (e.g., Feedback, DateInput, Checkbox). All three reviewers agreed the code is clean and idiomatic.

Strengths (consensus):

  • The FileUploader class uses the @dataclass(repr=False) pattern consistent with all other widget classes.
  • The InitialValue sentinel for state tracking matches established patterns.
  • The _register_uploaded_files method is cleanly separated; the register_file helper avoids exposing private attributes.
  • Docstrings are thorough and follow NumPy style.
  • File persistence via _get_files_to_register() fallback to session state is well-designed.

Issues identified:

  1. Dead code in test (raised by opus-4.6-thinking): test_file_uploader_multiple_persists_across_runs (line 1275) defines a local script() function that is never used — the test passes _multi_file_script to AppTest.from_function() instead. This is clearly leftover development code and should be cleaned up. Severity: Low — non-blocking.

  2. upload() discards persisted files across reruns (raised by gpt-5.3-codex-high as blocking, opus-4.6-thinking as minor): When _files is InitialValue (after a rerun reconstructs the widget), calling upload() replaces it with [] and appends only the new file, losing previously persisted files from session state. Verified by code inspection: Lines 997-999 of element_tree.py confirm that upload() resets to an empty list when the state is InitialValue, bypassing the session-state fallback in _get_files_to_register().

    Resolution: After verification, this is a valid but non-blocking concern. The upload() method is designed for building up a batch of files before a run(), consistent with how set_value() fully replaces the file list. A user who wants to preserve previously uploaded files across reruns should re-upload all files or use set_value() with the full list. The behavior is internally consistent, but the docstring should clarify this. This does not warrant blocking the merge.

  3. set_value tuple heuristic (raised by opus-4.6-thinking): The check isinstance(files, tuple) and len(files) == 3 and isinstance(files[0], str) to distinguish single vs. multiple files is pragmatic but could be fragile in theory. In practice the isinstance(files[0], str) guard makes false matches extremely unlikely. Severity: Low — acceptable design trade-off.

Test Coverage

All reviewers agreed the test coverage is strong. Tests cover:

  • Single file upload with content/type/size verification
  • Multiple file uploads via set_value()
  • Incremental uploads via upload() method chaining
  • File clearing via clear()
  • Key-based widget access
  • Persistence across runs (single and multiple files)
  • repr() safety

Gaps identified:

  • No test for upload() -> run() -> upload() -> run() across reruns (gpt-5.3-codex-high, opus-4.6-thinking). This would document the current behavior where upload() starts fresh per batch.
  • No test for set_value(None) explicitly, or for allowed_type / accept_directory properties (opus-4.6-thinking).
  • No negative/edge-case tests for empty file content, uploading to a single-file widget when a file exists, etc. (opus-4.6-thinking).

These are enhancement suggestions, not blocking gaps.

Backwards Compatibility

Consensus: No breaking changes. The change is purely additive — new FileUploader class, new properties on Block/ElementTree/AppTest, new register_file method on LocalScriptRunner, and a new elif branch in parse_tree_from_messages. Previously, st.file_uploader elements would appear as UnknownElement in AppTest.

Security & Risk

Consensus: No security concerns. All changes are confined to the AppTest testing framework, using MemoryUploadedFileManager (in-memory, no filesystem/network access). No server endpoints, authentication, CORS, or session transport paths are affected. Regression risk is low.

External test recommendation

  • Recommendation: No
  • Triggered categories: None
  • Evidence: All changes are in lib/streamlit/testing/v1/ (AppTest framework) and lib/tests/ (unit tests). No frontend, server, routing, auth, embedding, or asset-serving code is affected.
  • Confidence: High (all three reviewers agreed)

Accessibility

Not applicable. No frontend changes in this PR.

Recommendations

  1. Remove dead code in test (element_tree_test.py, line 1275): Remove the unused local script() function in test_file_uploader_multiple_persists_across_runs, or use it instead of _multi_file_script.
  2. Document upload() cross-rerun behavior: Add a note to the upload() docstring clarifying it starts with a fresh file list (not additive to previously persisted files from session state).
  3. Consider adding a cross-rerun upload() test: A test doing upload() → run() → upload() → run() would document the expected behavior and prevent future confusion.

Reviewer Agreement Summary

Topic gemini-3.1-pro gpt-5.3-codex-high opus-4.6-thinking
Code quality Good Good (with issue) Good (with issues)
Test coverage Excellent Strong (with gap) Comprehensive (with gaps)
Backwards compat No breaks No breaks No breaks
Security No concerns No concerns No concerns
External tests Not needed Not needed Not needed
upload() cross-rerun Not flagged Blocking Minor/non-blocking
Dead test code Not flagged Not flagged Flagged (minor)
Verdict APPROVED CHANGES REQUESTED APPROVED

Verdict

APPROVED: The implementation is well-designed, thoroughly tested, and follows existing codebase patterns. Two of three reviewers approved. The upload() cross-rerun behavior flagged by gpt-5.3-codex-high was verified as valid but non-blocking — the method's semantics are internally consistent (batch-oriented, like set_value()), and the concern is best addressed with a docstring clarification and an optional additional test, not a code change that could alter the API contract. The dead code in the test is a minor cleanup item. Neither issue rises to the level of blocking the merge.


This is a consolidated AI review by opus-4.6-thinking.


📋 Review by `gemini-3.1-pro`

Summary

This PR adds support for st.file_uploader to the AppTest testing framework, allowing developers to programmatically upload files and test their application's file handling logic.

Code Quality

The code is well-structured and follows the existing patterns for AppTest widgets. The FileUploader class in lib/streamlit/testing/v1/element_tree.py provides a clean API (set_value, upload, clear) for interacting with the widget. The integration with LocalScriptRunner to register files with the MemoryUploadedFileManager is handled correctly.

Test Coverage

The PR includes comprehensive unit tests in lib/tests/streamlit/testing/element_tree_test.py that cover single file uploads, multiple file uploads, incremental uploads (via upload()), clearing files, key-based access, and persistence across runs. The test coverage is excellent.

Backwards Compatibility

These changes are purely additive to the AppTest framework and do not modify any existing runtime behavior or public APIs outside of the testing utilities. There are no breaking changes.

Security & Risk

There are no security concerns or regression risks identified. The changes are confined to the testing framework and use the existing MemoryUploadedFileManager which is designed for this purpose.

External test recommendation

Recommendation: No
Triggered categories: None
Key evidence: The changes are entirely within the AppTest framework (lib/streamlit/testing/v1/ and lib/tests/streamlit/testing/), which is used for writing automated tests. They do not affect the runtime behavior, routing, or embedding of Streamlit apps.
Confidence: High. The modifications are isolated to testing utilities.

Accessibility

N/A - This PR does not contain frontend changes.

Recommendations

None. The implementation is solid and well-tested.

Verdict

APPROVED: The implementation of st.file_uploader support for AppTest is well-designed, thoroughly tested, and ready for merge.


This is an automated AI review by gemini-3.1-pro.

📋 Review by `gpt-5.3-codex-high`

Summary

This PR adds st.file_uploader support to AppTest by introducing a FileUploader test widget in element_tree, wiring file registration into AppTest/LocalScriptRunner, and adding Python unit tests for single/multiple uploads, clearing, persistence, and key-based access.

Code Quality

Overall structure is clean and follows existing AppTest patterns, but there is one blocking behavior issue:

  • lib/streamlit/testing/v1/element_tree.py:997-999 initializes self._files to [] when upload() is called and the widget is in InitialValue state. After a rerun, widgets are reconstructed with InitialValue, so calling upload() again drops previously uploaded files instead of appending to them. This diverges from additive multi-file uploader behavior and from the method's implied semantics.

Test Coverage

Coverage is strong for core paths (single upload, multi upload via set_value, clear, persistence, key access).

  • Gap: no test currently validates incremental upload() behavior across reruns. lib/tests/streamlit/testing/element_tree_test.py:1169-1180 only chains upload() calls before a single run(), so the regression described above is not caught.

Backwards Compatibility

No breaking changes to production st.file_uploader behavior are introduced; changes are scoped to AppTest internals and test utilities. The risk is limited to correctness of the newly added AppTest API behavior.

Security & Risk

No direct security-sensitive runtime paths were modified (no server endpoint/auth/cookie/CORS/session transport changes in production code). Main risk is functional mismatch in test simulation for additive uploads, which can lead to false confidence in app tests.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • lib/streamlit/testing/v1/app_test.py: AppTest-only file registration wiring.
    • lib/streamlit/testing/v1/element_tree.py: AppTest widget model/state handling for file uploader.
    • lib/streamlit/testing/v1/local_script_runner.py: typed helper around in-memory uploaded file manager in test runner.
    • lib/tests/streamlit/testing/element_tree_test.py: unit tests only.
  • Suggested external_test focus areas:
    • None required for this PR; validate with unit tests in AppTest subsystem.
    • If desired, add a local regression unit test for multi-file additive uploads across reruns.
  • Confidence: High
  • Assumptions and gaps: Assessment assumes no additional unshown changes outside this PR and no production server/runtime route/auth modifications.

Accessibility

No frontend/UI code changes were made (frontend/ untouched), so there are no new accessibility impacts in this PR.

Recommendations

  1. Update FileUploader.upload() to preserve existing uploaded files when current state is InitialValue (e.g., initialize from _get_files_to_register() instead of []).
  2. Add a regression test that does upload()->run()->upload()->run() for accept_multiple_files=True and asserts both files remain present.

Verdict

CHANGES REQUESTED: The new AppTest file uploader support is close, but upload() currently drops previously uploaded files across reruns, which should be fixed before merge.


This is an automated AI review by gpt-5.3-codex-high. Please verify the feedback and use your judgment.

📋 Review by `opus-4.6-thinking`

Summary

This PR adds st.file_uploader support to the AppTest testing framework, closing a long-standing feature request (#8093). The changes introduce:

  • A new FileUploader widget class in element_tree.py with set_value(), upload(), and clear() methods.
  • File registration logic in app_test.py that registers uploaded files with MemoryUploadedFileManager before each script run.
  • A register_file() helper on LocalScriptRunner to encapsulate file manager access.
  • Comprehensive unit tests covering single/multiple file uploads, incremental uploads, clearing, key-based access, and file persistence across runs.

Code Quality

The implementation is well-structured and closely follows existing patterns in the codebase (e.g., Feedback, DateInput, Checkbox).

Strengths:

  • The FileUploader class uses the @dataclass(repr=False) pattern consistent with all other widget classes.
  • The InitialValue sentinel for tracking explicit vs. implicit state matches the established pattern used by Feedback, DateInput, NumberInput, etc.
  • The _register_uploaded_files method is cleanly separated and the register_file helper on LocalScriptRunner avoids exposing private attributes.
  • Docstrings are thorough and follow NumPy style with proper examples.
  • The file persistence mechanism in _get_files_to_register() is well-designed — it falls back to reading UploadedFile objects from session state when no explicit set_value/upload/clear was called.

Issues:

  1. Dead code in test (lib/tests/streamlit/testing/element_tree_test.py, lines 1275–1283): test_file_uploader_multiple_persists_across_runs defines a local script() function that is never used. The test passes _multi_file_script to AppTest.from_function() instead (line 1285). The local script has an additional else: st.write("No files uploaded") branch that is never exercised. This is clearly leftover development code and should be cleaned up — either use the local script or remove it.

  2. upload() silently discards persisted files (element_tree.py, lines 997–999): When _files is InitialValue (i.e., files exist from a previous run), calling upload() replaces _files with an empty list and only adds the new file. This means previously uploaded files are lost. While this may be intentional (start fresh), it could be surprising to users who expect upload() to add to existing files across runs. Consider documenting this behavior or providing a way to add incrementally to persisted files.

  3. set_value heuristic for single vs. multiple files (element_tree.py, line 957): The check isinstance(files, tuple) and len(files) == 3 and isinstance(files[0], str) is pragmatic but could be fragile in edge cases (e.g., a filename that happens to be a tuple). In practice this is very unlikely given the isinstance(files[0], str) guard, but it's worth noting as a design trade-off.

Test Coverage

The tests are comprehensive and well-structured:

  • Single file upload with full content/type/size verification (test_file_uploader_single)
  • Multiple file uploads via set_value() (test_file_uploader_multiple)
  • Incremental uploads via upload() method (test_file_uploader_upload_method)
  • File clearing via clear() (test_file_uploader_clear)
  • Key-based widget access (test_file_uploader_with_key)
  • Persistence across runs for single and multiple files (test_file_uploader_persists_across_runs, test_file_uploader_multiple_persists_across_runs)
  • repr() safety verified in test_file_uploader_single

Gaps:

  • No test for passing None to set_value() explicitly (though clear() tests the equivalent _files = None behavior).
  • No test for the allowed_type or accept_directory properties (they're exposed but never asserted on except accept_multiple_files).
  • No negative/edge-case tests (e.g., uploading to a single-file widget when files already exist, empty file content, very large files).
  • E2E tests are not included, which is appropriate since this is a testing framework feature, not a user-facing UI element.

Backwards Compatibility

This change is purely additive with no breaking changes:

  • New FileUploader class added to element_tree.py.
  • New file_uploader property added to Block, ElementTree, and AppTest.
  • New register_file method on LocalScriptRunner.
  • New private _register_uploaded_files method on AppTest.
  • parse_tree_from_messages extended with a new elif branch for file_uploader.

Previously, st.file_uploader elements would have been parsed as UnknownElement in AppTest. This change properly recognizes them with a dedicated class. No existing APIs are modified or removed.

Security & Risk

No security concerns. All changes are confined to the AppTest testing framework, which runs locally in unit tests:

  • No network endpoints, authentication, or security-sensitive server paths are affected.
  • The mock URLs in _widget_state (e.g., /mock/upload/test session id/{file_id}) are just protobuf string values used in the in-memory test runner — they are not actual HTTP endpoints.
  • File registration uses MemoryUploadedFileManager, which is an in-memory storage with no filesystem or network access.
  • The cast in register_file is safe because LocalScriptRunner always creates a MemoryUploadedFileManager.

Regression risk is low. The change is additive and does not modify existing widget handling or script running logic.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • All changes are in lib/streamlit/testing/v1/ (AppTest framework) and lib/tests/ (unit tests) — no server, routing, auth, embedding, asset serving, or runtime changes.
    • File uploads use MemoryUploadedFileManager (in-memory) with no real HTTP endpoints.
    • No frontend code is changed.
  • Suggested external_test focus areas: None
  • Confidence: High
  • Assumptions and gaps: None — this is purely a Python-side testing framework enhancement with no deployment surface.

Accessibility

Not applicable. There are no frontend changes in this PR. The FileUploader class is a backend-only testing representation that does not render any UI.

Recommendations

  1. Remove dead code in test: In test_file_uploader_multiple_persists_across_runs (element_tree_test.py, line 1275), remove the unused local script() function or use it instead of _multi_file_script. Currently the local script (which includes an else branch for "No files uploaded") is defined but never called.

  2. Document upload() behavior with persisted files: Consider adding a note to the upload() docstring clarifying that it starts with a fresh file list (discarding any previously persisted files from session state). Alternatively, add a test that demonstrates this behavior.

  3. Consider adding edge-case tests: A test for set_value(None) and tests asserting the allowed_type property would improve confidence. A test for uploading an empty file (b"") would cover a boundary condition.

Verdict

APPROVED: Well-implemented, additive feature that follows existing codebase patterns with comprehensive tests. The dead code in the test and the upload() persistence behavior are minor issues that don't block merging.


This is an automated AI review by opus-4.6-thinking.

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Mar 11, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Copy link
Copy Markdown
Collaborator

@sfc-gh-lwilby sfc-gh-lwilby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@lukasmasuch lukasmasuch merged commit 2271764 into develop Mar 12, 2026
57 checks passed
@lukasmasuch lukasmasuch deleted the lukasmasuch/apptest-file-uploader branch March 12, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:feature PR contains new feature or enhancement implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[AppTest] Add file_upload widget

3 participants