Upload attributes not cleared after callback runs

I’ve submitted this as an issue (#818) but wanted to share it here in case it rings any bells with anyone.

It might be related to #816.


I have a callback that is responsible for adding or removing “jobs” from a list.

It has two inputs and a state. The first input is an Upload, if it is not None then the files are added to the list of jobs. The second input is a button and the state is the value of a Dropdown with multi set to true. If the button is pressed then the jobs from the Dropdown are removed from the list.

When a file is dropped into the Upload, the callback fires and the file is added to the list of jobs.

When the button is pressed, it turns out that the previously uploaded filename is still present in the filenames Input variable so it ends up getting re-added. If that file had been chosen for deletion then it is both deleted and added, resulting in no-change. If a different file had been chosen for deletion that the first file is added to the list again, resulting in a duplication.

I can work around this by checking the context to see what triggered the callback, rather than depending on the state of the Input variables.

I AssUMe-ed that once the callback ran the Inputs/States related to the Upload would be cleared or reset or … Am I mistaken?

It’s possible that this also explains Issue #816; perhaps the Upload component or Dropzone is seeing a file that is the same as the current contents of those variables and therefor does nothing.

There is a full example of this behavior in this gist. Here’s the callback:

@app.callback(
    Output("jobs", "data"),
    [
        Input("upload-data", "filename"),
        Input("delete-jobs-button", "n_clicks"),
    ],
    [State("jobs-to-delete", "value"), State("jobs", "data")],
)
def update_jobs(new_jobs, button, delible_jobs, jobs):
    ctx = dash.callback_context
    print("---")
    print(f"ctx.triggered: {ctx.triggered}")
    print(f"Delible: {delible_jobs}")
    print(f"New: {new_jobs}")
    print(f"Jobs: {jobs}")

    if new_jobs is None and delible_jobs is None:
        raise PreventUpdate

    jobs = jobs or []
    if delible_jobs is not None:
        jobs = [j for j in jobs if j not in delible_jobs]

    if new_jobs is not None:
        jobs += new_jobs
    return jobs

To my understanding this is the expected behaviour: No callbacks ever automatically “clear/reset” the Input(s) or State(s).

The dcc.Upload component does not currently have methods to clear the filename (or contents) property. What you may do is checking the context what caused the callback to be triggered (as you suggested).

Thanks. This is a pretty big ah-ha moment for me that probably will help with several other icky corners I’ve painted myself into.

I incorrectly believed that only the Inputs that triggered a callback would have values.

I’ve reread the Determining which Input has fired with dash.callback_context section of the Advanced Callbacks page and the Multiple inputs section of the Basic Dash Callbacks page. I can see that the behavior is expected and actually necessary.

It still seems like this might shed light into what’s going on in Issue #816.

THANK YOU!

Regarding to the re-uploading of files, you might be interested in checking out dash-uploader (I’m the author). I just bumped v.0.3.0 in which re-uploading files with same filename is possible.

The working principle of the component is a bit different (than in dcc.Upload), since it actually sends the files to the hard drive of the server.

1 Like

Thanks for the pointer to your uploader! It looks like it could be useful.

try to put the upload into the output then when you are about to clear the data set contents and the filename becomes an empty string

@dash.callback(
    Output('file-name-data-upload', 'children'),
    Output('file-name-data-upload-container', 'style'),
    Output('upload-data', 'contents'),
    Output('upload-data', 'filename'),
    Input('upload-data', 'contents'),
    Input('btn-close-file-uploaded', 'n_clicks'),
    State('upload-data', 'filename'),
    prevent_initial_call=True
)
def update_output(list_of_contents, n_clicks, list_of_names):
    triggered_id = ctx.triggered_id
    nama_file = list_of_names
    
    if triggered_id == 'btn-close-file-uploaded':
        # reset upload data, return emty string to clear
        return '', {'visibility': 'hidden', 'display': 'none'}, '', ''

    elif triggered_id == 'upload-data':
        nama_file = list_of_names
        c = [
            parse_contents(
                list_of_contents,
                list_of_names,
            )
        ]
        return nama_file, {'visibility': 'visible', 'display': 'flex'}, list_of_contents, list_of_names
    
    else:
        raise PreventUpdate

but maybe this method is not effective.