Submit Form containing file_uploader (with multiple files = True) needs to be called 2 times for it to work

Summary

I have a form with file uploader with option to upload multiple files. When I click submit with callback method. The first time, the callback method passes empty file list and only second time is the file list passed

Steps to reproduce

Code snippet:

def save_files(files):
    print('inside save_files: ', files)
    doc_dir = 'documents'
    if len(files) > 0:
        for file in files:
            with open(os.path.join(doc_dir, file.name), 'wb') as f:
                f.write(file.getbuffer())

            st.session_state.uploaded_files.append({'Name': file.name,
                                                    'Type': file.type,
                                                    'Size': str(file.size),
                                                    'Summary': 'TBC'})

        st.session_state.disabled = True
        return True
    else:
        st.session_state.disabled = False
        return False

def main():
    col1, col2 = st.columns(2)

    st.sidebar.title('File Upload and Processing')
    with st.sidebar.form(key='sidebar_form'):
        # Allow the user to upload a files
        uploaded_files = st.file_uploader('Upload files',
                                          type=['pdf', 'txt'],
                                          key='file_upload_widget',
                                          accept_multiple_files=True,
                                          disabled=st.session_state.disabled)

        # If a files was uploaded, display its contents
        submit_btn = st.form_submit_button('Upload Files',
                                           on_click=save_files,
                                           args=(uploaded_files,),
                                           disabled=st.session_state.disabled)
        if submit_btn:
            st.sidebar.write('No more upload possible')

As you can see save_files has print statment
on the terminal I see
On first Submit click

inside save_files:

On second time submit click

inside save_files: [UploadedFile(id=1, name=‘22080200056474.pdf’, type=‘application/pdf’, size=81932)]

Expected behavior:

On first Submit click, I should see on terminal

inside save_files: [UploadedFile(id=1, name=‘22080200056474.pdf’, type=‘application/pdf’, size=81932)]

Actual behavior:

I would expect that first Submit should pass the list of files uploaded (in this example I tried to upload only file but problem exist for multiple files also

There are two important points to understand here:

  1. When you put things in a form, their output value does not update until you submit the form. In your case, the variable uploaded_files is an empty list until after the form submit button is clicked, at which point the page will reload and it will get updated with the new output value reflecting the user’s submission.

  2. The arguments passed to callback functions are populated when the widget is rendered on the page and not at the later point when the callback is executed.

So when your page loads, that callback function is all queued up and ready to pass that empty list of files that is the default value, no matter what a user has done inside the form. When the page reloads from that first submission, the callback function is all queued up and ready to pass that file list that was saved to the widget output from the first submission.

In general for callbacks, if you need to do something with the “new value” within the callback, you won’t be able to pass it as an argument; you’re going to have to access it by session state using the widget’s key. This is true for a collection of widgets within a form, or for a single, stand-alone widget executing a callback.

ok, thank you for your clarification. so as i understand
I should use callback of upload_files to ‘manually’ update the list of files in a session state and use this session state for file processing (in my case copying to the folder).

I will update here if i get it to work

You already have the key file_upload_widget assigned to the file uploader. So, within the body of your callback function (not passed as an argument), you can access the files via st.session_state.file_upload_widget. In this case, your save_files function would not longer take an argument.

If you need to reuse the function for other purposes, you could pass the string that is the widget key as an argument and then within the function get the files associated to the key; you just have to make sure you are looking up the value of a widget from session state within the body of the callback and not before (not passed as an argument) if the callback is for that same widget.

1 Like

oh, yes, you are right 
that was easy. thank you, so my code is now simply

def save_files():
    files =st.session_state.file_upload_widget
    print('inside save_files: ', files)
    doc_dir = 'documents'
    if len(files) > 0:
        for file in files:
            with open(os.path.join(doc_dir, file.name), 'wb') as f:
                f.write(file.getbuffer())

            st.session_state.uploaded_files.append({'Name': file.name,
                                                    'Type': file.type,
                                                    'Size': str(file.size),
                                                    'Summary': 'TBC'})

        st.session_state.disabled = True
        return True
    else:
        st.session_state.disabled = False
        return False

def main():
    col1, col2 = st.columns(2)

    st.sidebar.title('File Upload and Processing')
    with st.sidebar.form(key='sidebar_form'):
        # Allow the user to upload a files
        uploaded_files = st.file_uploader('Upload files',
                                          type=['pdf', 'txt'],
                                          key='file_upload_widget',
                                          accept_multiple_files=True,
                                          disabled=st.session_state.disabled)

        # If a files was uploaded, display its contents
        submit_btn = st.form_submit_button('Upload Files',
                                           on_click=save_files,
                                           disabled=st.session_state.disabled)
        if submit_btn:
            st.sidebar.write('No more upload possible')```

just don’t get your last line though


and not before (not passed as an argument) if the callback is for that same widget.

If you pass either uploaded_files or st.session_state.file_upload_widget as an argument to the file upload widget’s callback (or the submit button’s callback when it’s contained in a form), that’s how you get the appearance of a “delay”. You have to retrieve the value directly from session state within the callback function and can’t have the value passed to the callback function as an argument. If you have multiple widgets on the page (outside of a form), you can pass other widget’s values as arguments to a callback function. In order to capture a widget’s own value that is newly changed and triggering a callback, that’s where your hands are tied.

(In the case of a form, consider all the widgets contained in the form as logically tied to the submit button, so none of them would count as “other” widgets for the sake of passing their values as arguments to the submit button’s callback.)

1 Like

(In the case of a form, consider all the widgets contained in the form as logically tied to the submit button, so none of them would count as “other” widgets for the sake of passing their values as arguments to the submit button’s callback.)

perfect, all clear, thank you for the explanation!!

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.