Black Lives Matter. Please consider donating to Black Girls Code today.

Callback debugging

With a change that went into Dash recently, it’s possible now to introspect details of registered callback functions. I’ve taken advantage of this to make a function which prints out the details of all registered callbacks:

    callback      router @ router.py:25       
    Output        dash-container.children     
    Inputs     1  url.pathname                
    States     0     
    Events     0     
                     

    callback      update_nav @ router.py:34   
    Output        navbar.children             
    Inputs     1  url.pathname                
    States     0     
    Events     0     
                     

    callback      callback @ app.py:10        
    Output        graph.figure                
    Inputs     3  normalize.value, sort-type.value, text-input.value                            
    States     0     
    Events     0     

This is particularly useful for larger apps where it can be harder to keep track of callbacks, especially if they are being created in bulk. It’s also helpful for debugging the callback logic, where the first thing I want to do when reading someone else code is work out what callbacks are defined and what their input and output targets are, before working out what they return. If this is something people found useful, I could put together a PR.

def show_callbacks(app):

    def format_regs(registrations, padding=10):
        # TODO: -- switch to single line printing if > 79 chars                                                                                                                                
        vals = sorted("{}.{}".format(i['id'], i['property'])
                      for i in registrations)
        return ", ".join(vals)

    output_list = []

    for callback_id, callback in app.callback_map.items():
        wrapped_func = callback['callback'].__wrapped__
        inputs = callback['inputs']
        states = callback['state']
        events = callback['events']

        str_values = {
            'callback': wrapped_func.__name__,
            'output': callback_id,
            'filename': os.path.split(wrapped_func.__code__.co_filename)[-1],
            'lineno': wrapped_func.__code__.co_firstlineno,
            'num_inputs': len(inputs),
            'num_states': len(states),
            'num_events': len(events),
            'inputs': format_regs(inputs),
            'states': format_regs(states),
            'events': format_regs(events)
        }

        output = """                                                                                                                                                                           
        callback      {callback} @ {filename}:{lineno}                                                                                                                                         
        Output        {output}                                                                                                                                                                 
        Inputs  {num_inputs:>4}  {inputs}                                                                                                                                                      
        States  {num_states:>4}  {states}                                                                                                                                                      
        Events  {num_events:>4}  {events}                                                                                                                                                      
        """.format(**str_values)

        output_list.append(output)
    return "\n".join(output_list)
10 Likes

Thanks for posting this. I added this inside my dash app that has quite a few callbacks but get the following error when I run it. ‘function’ object has no attribute ‘wrapped

Any ideas on what is wrong?

Ah the change to Dash necessary to support this has not yet gone into the latest release. If you’re keen to access this now you can install the latest version from github or just manually patch dash.py as per this diff, as it’s only a two line change.

1 Like

Thanks very much for the info. I’ll give it a try.

1 Like

Nice job @nedned ! I took this function and wired it up to Graphvis to make this little Dash component that shows you the callback chain in visual form: https://github.com/nicolaskruchten/dash_callback_chain

here’s a sample:

8 Likes

This is very cool. I like it.

1 Like

This is insanely cool

Awsome. really helpful

There’s been a few changes to Dash since I posted this snippet, notably including the removal of Events and the addition of multi outputs. Here’s an updated version of this function that handles these changes:

def show_callbacks(app):

    def wrap_list(items, padding=24):
        return ("\n"+" "*padding).join(items)

    def format_regs(registrations):
        vals = sorted("{}.{}".format(i['id'], i['property'])
                      for i in registrations)
        return wrap_list(vals)

    output_list = []

    for callback_id, callback in app.callback_map.items():
        wrapped_func = callback["callback"].__wrapped__
        inputs = callback["inputs"]
        states = callback["state"]

        if callback_id.startswith(".."):
            outputs = callback_id.strip(".").split("...")
        else:
            outputs = [callback_id]

        str_values = {
            "callback": wrapped_func.__name__,
            "outputs": wrap_list(outputs),
            "filename": os.path.split(wrapped_func.__code__.co_filename)[-1],
            "lineno": wrapped_func.__code__.co_firstlineno,
            "num_inputs": len(inputs),
            "num_states": len(states),
            "inputs": format_regs(inputs),
            "states": format_regs(states),
            "num_outputs": len(outputs),
        }

        output = dedent(
            """                                                                                                                                                                                                      
            callback    {callback} @ {filename}:{lineno}                                                                                                                                                             
            Outputs{num_outputs:>3}  {outputs}                                                                                                                                                                       
            Inputs{num_inputs:>4}  {inputs}                                                                                                                                                                          
            States{num_states:>4}  {states}                                                                                                                                                                          
            """.format(**str_values)
        )

        output_list.append(output)
    return "\n".join(output_list)

@nedned I’m curious, have you come up with a witty solution on using an interactive python debugger in callbacks?

Since dash is threaded, there is no way for me to set a trace and jump into an interactive session where I can inspect the code.

Using ipdb has worked for me:
import ipdb; ipdb.set_trace()

Also, the debugger in VSCode has also worked for me.

Of course it now works for me, but most the time, me setting a trace in a running dash app results in the program getting hung up. (I’ll try to post an example next time I run into that)

Has this never happened to you?

I have seen this before, but I think only when running with multiple workers or threads. Definitely make sure you’re using a single thread/worker when you run your app for debugging.

I usually have a command set up to run Dash in prod mode (eg with gunicorn and multiple workers) and a command to run for debugging (eg using Flask dev server). If you want to debug your prod environment while running with gunicorn etc you need to make sure you’re using a single worker/thread and also disable/extend the request timeout, otherwise the WSGI server will kill and respawn the worker.

1 Like

Yes, when I pass threaded=False to run_server it now works fine.

Thanks for the help!

2 Likes