Repository: streamlit/streamlit

Issue:
File uploader does not return multiple files
From: https://discuss.streamlit.io/t/issue-with-file-uploader-and-streamlit-version-0-84-0/14812

---

Hi everyone,

I'm new here. First of all thank you really much for your great work in this forum. It helped me out multiple times. But now I encountered an issue where I couldn't find a thread on, yet.

With the latest version (0.84.0) I'm experiencing a problem with the file_uploader widget.

In the prior version (0.82.0) it was very handy, for my case, to select one file after another and drag&drop it to the widget. With the latest version this doesn't seems possible anymore. When one or more files were uploaded the next file/s draged and droped to the widget are not accessible, even though these files appear in the interactive list below the widget. 
I want to use st.session_state is the app I'm working on and for that reason the version 0.84.0 is necessary to my understanding.

In order for you to reproduce the situation I made up this example:

```
import streamlit as st

uploaded_files = st.file_uploader('Select files',type=['txt'],accept_multiple_files=True)
file_lst = [uploaded_file.name for uploaded_file in uploaded_files]
st.write(file_lst)
```

Assuming I want to upload the two files test1.txt and test2.txt one after another. For the first file (test1.txt) the behavior is as expected and equivalent for both versions:

![6af23e3b957ef45b0307c34bf571a0ce940a4714](https://user-images.githubusercontent.com/690814/125117909-c091be00-e0a3-11eb-9a52-5ce303cc18f3.jpeg)

Then later I want to upload another file in this case test2.txt. 
The expected behavior can be seen with version 0.82.0. Both files are shown in the interactive list below the widget as well as in the written file_lst.

![565a2eb34e7e1a6ef1d31892f932edb32bd41313](https://user-images.githubusercontent.com/690814/125117918-c4254500-e0a3-11eb-98e2-e51924314cff.jpeg)

With version 0.84.0 only the interactive list below the widget shows both files. The written file_lst shows only test1.txt.

![95481b85583aab65fa13eb7cdaf0602c69ed7610](https://user-images.githubusercontent.com/690814/125117923-c6879f00-e0a3-11eb-9b84-925d509c7ed5.jpeg)

Anyone had a similar issue? I apologize, if the solution is obvious, but I got stuck with it and can't figure out, how to solve the issue.


Discussion:

Resolving patch:
diff --git a/lib/streamlit/elements/file_uploader.py b/lib/streamlit/elements/file_uploader.py
--- a/lib/streamlit/elements/file_uploader.py
+++ b/lib/streamlit/elements/file_uploader.py
@@ -167,10 +167,15 @@ def serialize_file_uploader(
                 ids = [f.id for f in files]
             else:
                 ids = [files.id]
+
             ctx = get_report_ctx()
             if ctx is None:
                 return []
-            max_id = ctx.uploaded_file_mgr._file_id_counter
+
+            # ctx.uploaded_file_mgr._file_id_counter stores the id to use for
+            # the next uploaded file, so the current highest file id is the
+            # counter minus 1.
+            max_id = ctx.uploaded_file_mgr._file_id_counter - 1
             return [max_id] + ids
 
         # FileUploader's widget value is a list of file IDs
@@ -187,6 +192,22 @@ def serialize_file_uploader(
             serializer=serialize_file_uploader,
         )
 
+        ctx = get_report_ctx()
+        if ctx is not None and widget_value:
+            serialized = serialize_file_uploader(widget_value)
+
+            # The first number in the serialized widget_value list is the id
+            # of the most recently uploaded file.
+            newest_file_id = serialized[0]
+            active_file_ids = list(serialized[1:])
+
+            ctx.uploaded_file_mgr.remove_orphaned_files(
+                session_id=ctx.session_id,
+                widget_id=file_uploader_proto.id,
+                newest_file_id=newest_file_id,
+                active_file_ids=active_file_ids,
+            )
+
         self.dg._enqueue("file_uploader", file_uploader_proto)
         return widget_value
 
@@ -208,27 +229,15 @@ def _get_file_recs(
             )
             return []
 
-        # The first number in the widget_value list is 'newestServerFileId'
-        newest_file_id = widget_value[0]
         active_file_ids = list(widget_value[1:])
 
         # Grab the files that correspond to our active file IDs.
-        file_recs = ctx.uploaded_file_mgr.get_files(
+        return ctx.uploaded_file_mgr.get_files(
             session_id=ctx.session_id,
             widget_id=widget_id,
             file_ids=active_file_ids,
         )
 
-        # Garbage collect "orphaned" files.
-        ctx.uploaded_file_mgr.remove_orphaned_files(
-            session_id=ctx.session_id,
-            widget_id=widget_id,
-            newest_file_id=newest_file_id,
-            active_file_ids=active_file_ids,
-        )
-
-        return file_recs
-
     @property
     def dg(self) -> "streamlit.delta_generator.DeltaGenerator":
         """Get our DeltaGenerator."""


Test patch:
diff --git a/lib/tests/streamlit/file_uploader_test.py b/lib/tests/streamlit/file_uploader_test.py
--- a/lib/tests/streamlit/file_uploader_test.py
+++ b/lib/tests/streamlit/file_uploader_test.py
@@ -15,11 +15,13 @@
 """file_uploader unit test."""
 
 from unittest.mock import patch
+
 import pytest
 
 import streamlit as st
 from streamlit import config
 from streamlit.proto.Common_pb2 import SInt64Array
+from streamlit.report_thread import get_report_ctx
 from streamlit.uploaded_file_manager import UploadedFileRec, UploadedFile
 from tests import testutil
 
@@ -46,10 +48,8 @@ def test_multiple_types(self):
         c = self.get_delta_from_queue().new_element.file_uploader
         self.assertEqual(c.type, [".png", ".svg", ".jpeg"])
 
-    @pytest.mark.skip  # the role of register_widget changed with session state, so this needs to be rewritten
-    @patch("streamlit.uploaded_file_manager.UploadedFileManager.get_files")
-    @patch("streamlit.elements.file_uploader.register_widget")
-    def test_multiple_files(self, register_widget_patch, get_files_patch):
+    @patch("streamlit.elements.file_uploader.FileUploaderMixin._get_file_recs")
+    def test_multiple_files(self, get_file_recs_patch):
         """Test the accept_multiple_files flag"""
         # Patch UploadFileManager to return two files
         file_recs = [
@@ -57,11 +57,7 @@ def test_multiple_files(self, register_widget_patch, get_files_patch):
             UploadedFileRec(2, "file2", "type", b"456"),
         ]
 
-        get_files_patch.return_value = file_recs
-
-        # Patch register_widget to return the IDs of our two files
-        file_ids = [rec.id for rec in file_recs]
-        register_widget_patch.return_value = (file_ids, False)
+        get_file_recs_patch.return_value = file_recs
 
         for accept_multiple in [True, False]:
             return_val = st.file_uploader(
@@ -102,12 +98,8 @@ def test_max_upload_size_mb(self):
             c.max_upload_size_mb, config.get_option("server.maxUploadSize")
         )
 
-    @pytest.mark.skip  # the role of register_widget changed with session state, so this needs to be rewritten
-    @patch("streamlit.uploaded_file_manager.UploadedFileManager.get_files")
-    @patch("streamlit.elements.file_uploader.register_widget")
-    def test_unique_uploaded_file_instance(
-        self, register_widget_patch, get_files_patch
-    ):
+    @patch("streamlit.elements.file_uploader.FileUploaderMixin._get_file_recs")
+    def test_unique_uploaded_file_instance(self, get_file_recs_patch):
         """We should get a unique UploadedFile instance each time we access
         the file_uploader widget."""
 
@@ -117,11 +109,7 @@ def test_unique_uploaded_file_instance(
             UploadedFileRec(2, "file2", "type", b"456"),
         ]
 
-        get_files_patch.return_value = file_recs
-
-        # Patch register_widget to return the IDs of our two files
-        file_ids = [rec.id for rec in file_recs]
-        register_widget_patch.return_value = (file_ids, False)
+        get_file_recs_patch.return_value = file_recs
 
         # These file_uploaders have different labels so that we don't cause
         # a DuplicateKey error - but because we're patching the get_files
@@ -136,34 +124,34 @@ def test_unique_uploaded_file_instance(
         self.assertEqual(b"3", file1.read())
         self.assertEqual(b"123", file2.read())
 
-    @pytest.mark.skip  # the role of register_widget changed with session state, so this needs to be rewritten
     @patch("streamlit.uploaded_file_manager.UploadedFileManager.remove_orphaned_files")
-    @patch("streamlit.elements.file_uploader.register_widget")
+    @patch("streamlit.elements.file_uploader.FileUploaderMixin._get_file_recs")
     def test_remove_orphaned_files(
-        self, register_widget_patch, remove_orphaned_files_patch
+        self, get_file_recs_patch, remove_orphaned_files_patch
     ):
         """When file_uploader is accessed, it should call
         UploadedFileManager.remove_orphaned_files.
         """
-        newest_file_id = 100
-        active_file_ids = [41, 42, 43]
+        ctx = get_report_ctx()
+        ctx.uploaded_file_mgr._file_id_counter = 101
 
-        # Patch register_widget. The first value in the array is
-        # "newest_file_id". It's followed by all the active file IDs
-        file_ids = [newest_file_id] + active_file_ids
-        register_widget_patch.return_value = (file_ids, False)
+        file_recs = [
+            UploadedFileRec(1, "file1", "type", b"123"),
+            UploadedFileRec(2, "file2", "type", b"456"),
+        ]
+        get_file_recs_patch.return_value = file_recs
 
-        st.file_uploader("foo")
-        remove_orphaned_files_patch.assert_called_once_with(
-            session_id="test session id",
-            widget_id="",
-            newest_file_id=newest_file_id,
-            active_file_ids=active_file_ids,
-        )
+        st.file_uploader("foo", accept_multiple_files=True)
+
+        args, kwargs = remove_orphaned_files_patch.call_args
+        self.assertEqual(len(args), 0)
+        self.assertEqual(kwargs["session_id"], "test session id")
+        self.assertEqual(kwargs["newest_file_id"], 100)
+        self.assertEqual(kwargs["active_file_ids"], [1, 2])
 
-        # Patch to return None instead. remove_orphaned_files should not
-        # be called when file_uploader is accessed.
-        register_widget_patch.return_value = (None, False)
+        # Patch _get_file_recs to return [] instead. remove_orphaned_files
+        # should not be called when file_uploader is accessed.
+        get_file_recs_patch.return_value = []
         remove_orphaned_files_patch.reset_mock()
 
         st.file_uploader("foo")