plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.

Home Page:https://plotly.com/dash

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Feature Request] `dash.callback` should utilize `functools.wraps`

shea-parkes opened this issue · comments

Thanks so much for your interest in Dash!

Before posting an issue here, please check the Dash community forum to see if the topic has already been discussed. The community forum is also great for implementation questions. When in doubt, please feel free to just post the issue here :)

Is your feature request related to a problem? Please describe.
Yes. We implemented a profiling solution for some of our longer running callback functions. During implementation, we assumed that mycallback.__wrapped__ would exist. It did not, so we had to rework how we defined our callbacks.

Describe the solution you'd like
Use the python standard library functools.wraps inside of dash.callback to have standard access to the decorated function.

Describe alternatives you've considered
Tried to look at dash internals to see if there was a convenient way to access the original function. The dash.callback internals are complex so we chose not to do any deep bindings to non-public APIs.

We could switch to defining our callback functions without decoration, and then using dash.callback(..)(myfunc) (i.e. don't use dash.callback as a decorator`). But that's also adding complexity to the solution.

Additional context
Given that dash.callback is intended to be a decorator, I think it is reasonable to want it to implement the standard functools.wraps.

However, this is y'all's solution, and you're already providing great value to us, so I understand if you feel differently. Thanks!

In case anyone else stumbles here someday, here's the code snippet of how we're handling this currently. Instead of dash.callback, we made mylib.callback and use it in the same way:

P = typing.ParamSpec("P")  # Params to dash.callback (e.g. dash.Outputs, dash.Inputs)
F = typing.TypeVar(  # Signature of our function being passed to dash.callback
    "F", bound=typing.Callable[..., typing.Any]
)

@functools.wraps(dash.callback)  # Just meta-fun, not the usage of `functools.wraps` I'm asking about...
def callback(
    *dash_io_args: P.args,
    **dash_io_kwargs: P.kwargs,
) -> typing.Callable[[F], F]:
    """wraps dash.callback to enable introspection to access the underlying function"""

    def our_decorator(f: F) -> F:
        """The inner closure that nests two decorators (functools.wraps and dash.callback)"""
        return functools.wraps(f)(dash.callback(*dash_io_args, **dash_io_kwargs)(f))

    return our_decorator

Upon further review, it appears that the reason we didn't see a mycallback.__wrapped__ was that the callback wrapping functionality stores the wrapped function elsewhere, but then just returns back the original function from the decorator. I'm double checking, but I think that this means this could all be shut down. Sorry for the inconvenience.

Indeed, dash.callback() returns the original function unaltered and unwrapped. It does create a wrapped version, but it stores that in its own internal ~namespace. So I'm closing this down. Sorry y'all.

Thanks @shea-parkes - yes up to v2.3 we returned the wrapper but this was fixed in v2.4.0 via #1839