St.file_uploader keeps calling the callback function passed into the 'on_change' parameter on every other widget change

Streamlit v1.3.0

I only want the file_uploader to run the callback function when I remove any file or upload any new file to the file_uploader widget. But now I’m having this issue where it keeps calling the ‘on_change’ callback function even though I didn’t remove or upload any file. The way to trigger this is just to change any other widget’s value (e.g. the st.radio button in the example code below).

But I know I’m actually changing it’s value when I tried to access it to copy the file to a temporary folder to use it to do something. So I tried to use video_file.seek(0) and also deepcopy(video_file) to try to avoid changing the value of the file_uploader, but still in vain. Please help.

import streamlit as st
import shutil
import os
from copy import deepcopy
from pathlib import Path

TEMP_DIR = Path('tmp/extracted')


def dummy_cb():
    print("DUMMY CALLBACK FOR FILE UPLOADER")


video_file = st.sidebar.file_uploader(
    "Upload a video", type=['mp4', 'mov', 'avi', 'asf', 'm4v'],
    key='video_file_uploader', on_change=dummy_cb)

if video_file is None:
    st.stop()

# use this to avoid keep calling the file_uploader's callback
# NOTE: still not helping...
uploaded_video = deepcopy(video_file)

video_path = str(TEMP_DIR / uploaded_video.name)

print("Copying file")
if TEMP_DIR.exists():
    shutil.rmtree(TEMP_DIR)
os.makedirs(TEMP_DIR)
print(f"{video_path = }")
with open(video_path, 'wb') as f:
    f.write(uploaded_video.getvalue())

# seeking is also not helping
uploaded_video.seek(0)
video_file.seek(0)

st.radio("options", (1, 2), key='dummy_options')
st.stop()
1 Like

I settled with a workaround by performing all the necessary callbacks within the if video_file is None statement. This works well for accepting a single upload but probably not multiple files.

if video_file is None:
    logger.info("Resetting states for uploaded video")
    # reset and remove the TEMP_DIR if there's no uploaded file
    # to ensure the app only reads the new uploaded file
    reset_cb()
    if TEMP_DIR.exists():
        shutil.rmtree(TEMP_DIR)
    os.makedirs(TEMP_DIR)
    st.stop()

# using str to ensure readable by st.video and cv2
video_path = str(TEMP_DIR / video_file.name)

if not os.path.exists(video_path):
    # only creates it if not exists, to speed up the process when
    # there is any widget changes besides the st.file_uploader
    logger.debug(f"{video_path = }")
    with st.spinner("Copying video to a temporary directory ..."):
        with open(video_path, 'wb') as f:
            f.write(video_file.getvalue())
1 Like

Hi, I am having the same issue. I’ve created a demo app to replicate the issue so that it can hopefully be addressed sooner.

import streamlit as st

def uploader_callback():
    print('Uploaded file')

st.file_uploader(
    label='File uploader',
    on_change=uploader_callback,
    key='file_uploader'
)

st.text_input(label='Textbox 1', key='first')

Bug: If another widget gets updated, e.g. st.text_input, the callback function passed into ‘on_change’ of st.file_uploader gets reran even if the file has remained the same.

Desired: st.file_uploader on_change to behave as it should - only running when the value of st.file_uploader is changed