Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions dash/_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def callback(
optional: Optional[bool] = False,
hidden: Optional[bool] = None,
websocket: Optional[bool] = False,
persistent: Optional[bool] = False,
**_kwargs,
) -> Callable[..., Any]:
"""
Expand Down Expand Up @@ -172,6 +173,10 @@ def callback(
The endpoint is relative to the Dash app's base URL.
Note that the endpoint will not appear in the list of registered
callbacks in the Dash devtools.
:param persistent:
If True, this callback will not show the "Updating..." title while
running. Useful for persistent WebSocket callbacks that stay active
for long periods without requiring a loading indicator.
"""

background_spec: Any = None
Expand Down Expand Up @@ -230,6 +235,7 @@ def callback(
optional=optional,
hidden=hidden,
websocket=websocket,
persistent=persistent,
)


Expand Down Expand Up @@ -278,6 +284,7 @@ def insert_callback(
optional=False,
hidden=None,
websocket=False,
persistent=False,
) -> str:
if prevent_initial_call is None:
prevent_initial_call = config_prevent_initial_callbacks
Expand All @@ -304,6 +311,7 @@ def insert_callback(
"optional": optional,
"hidden": hidden,
"websocket": websocket,
"persistent": persistent,
}
if running:
callback_spec["running"] = running
Expand Down Expand Up @@ -658,6 +666,7 @@ def register_callback(
optional=_kwargs.get("optional", False),
hidden=_kwargs.get("hidden", None),
websocket=_kwargs.get("websocket", False),
persistent=_kwargs.get("persistent", False),
)

# pylint: disable=too-many-locals
Expand Down
14 changes: 14 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ def _concat(x):

if no_output:
# No output will hash the inputs.
# For no-input callbacks, also include the call site to make each unique
if not inputs:
# Get the call site of the @callback decorator
stack = inspect.stack()
# Walk up the stack to find the actual callback call site
# (skip internal dash package frames)
dash_package_path = os.path.dirname(__file__)
for frame_info in stack:
# Skip frames from within the dash package itself
if not frame_info.filename.startswith(dash_package_path):
call_site = f"{frame_info.filename}:{frame_info.lineno}"
return hashlib.sha256(call_site.encode("utf-8")).hexdigest()
# Fallback to empty hash if no external frame found
return _hash_inputs()
return _hash_inputs()

if isinstance(output, (list, tuple)):
Expand Down
7 changes: 5 additions & 2 deletions dash/dash-renderer/src/actions/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,17 @@ function validateDependencies(parsedDependencies, dispatchError) {
'In the callback for output(s):\n ' +
outputs.map(combineIdAndProp).join('\n ');

if (!inputs.length) {
if (!inputs.length && dep.prevent_initial_call) {
dispatchError('A callback is missing Inputs', [
head,
'there are no `Input` elements.',
'Without `Input` elements, it will never get called.',
'',
'Subscribing to `Input` components will cause the',
'callback to be called whenever their values change.'
'callback to be called whenever their values change.',
'',
'If you want a callback without inputs that fires on initial load,',
'set prevent_initial_call=False.'
]);
}

Expand Down
14 changes: 10 additions & 4 deletions dash/dash-renderer/src/actions/dependencies_ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,18 @@ export const getLayoutCallbacks = (

export const getUniqueIdentifier = ({
anyVals,
callback: {inputs, outputs, state}
}: ICallback): string =>
concat(
map(combineIdAndProp, [...inputs, ...outputs, ...state]),
callback: {inputs, outputs, state, output}
}: ICallback): string => {
const idParts = map(combineIdAndProp, [...inputs, ...outputs, ...state]);
// For no-output callbacks, include the output hash to ensure uniqueness
if (outputs.length === 0 && output) {
idParts.push(output);
}
return concat(
idParts,
Array.isArray(anyVals) ? anyVals : anyVals === '' ? [] : [anyVals]
).join(',');
};

export function includeObservers(
id: any,
Expand Down
67 changes: 61 additions & 6 deletions dash/dash-renderer/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {getAppState} from '../reducers/constants';
import {getAction} from './constants';
import * as cookie from 'cookie';
import {validateCallbacksToLayout} from './dependencies';
import {includeObservers, getLayoutCallbacks} from './dependencies_ts';
import {
includeObservers,
getLayoutCallbacks,
makeResolvedCallback,
resolveDeps
} from './dependencies_ts';
import {computePaths, getPath} from './paths';
import {recordUiEdit} from '../persistence';

Expand Down Expand Up @@ -95,12 +100,62 @@ function triggerDefaultState(dispatch, getState) {
);
}

dispatch(
addRequestedCallbacks(
getLayoutCallbacks(graphs, paths, layout.components, {
outputsOnly: true
})
const layoutCallbacks = getLayoutCallbacks(
graphs,
paths,
layout.components,
{
outputsOnly: true
}
);

// Also include no-output callbacks whose inputs are in the layout (or have no inputs)
const noOutputCallbacks = (graphs.callbacks || [])
.filter(cb => cb.noOutput && !cb.prevent_initial_call)
.map(cb => {
const resolved = makeResolvedCallback(cb, resolveDeps(), '');
resolved.initialCall = true;
return resolved;
})
.filter(cb => {
// If no inputs, always include (fires once on initial load)
if (cb.callback.inputs.length === 0) {
return true;
}
// Check if any input is in the layout
const inputs = cb.getInputs(paths);
return inputs.some(inp =>
Array.isArray(inp) ? inp.length > 0 : inp
);
});

// Also include no-input callbacks (with outputs) that should fire on initial load
const noInputCallbacks = (graphs.callbacks || [])
.filter(
cb =>
!cb.noOutput &&
cb.inputs.length === 0 &&
!cb.prevent_initial_call
)
.map(cb => {
const resolved = makeResolvedCallback(cb, resolveDeps(), '');
resolved.initialCall = true;
return resolved;
})
.filter(cb => {
// Check if any output is in the layout
const outputs = cb.getOutputs(paths);
return outputs.some(out =>
Array.isArray(out) ? out.length > 0 : out
);
});

dispatch(
addRequestedCallbacks([
...layoutCallbacks,
...noOutputCallbacks,
...noInputCallbacks
])
);
}

Expand Down
7 changes: 6 additions & 1 deletion dash/dash-renderer/src/observers/isLoading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ const observer: IStoreObserverDefinition<IStoreState> = {

const pendingCallbacks = getPendingCallbacks(callbacks);

const next = Boolean(pendingCallbacks.length);
// Filter out persistent callbacks - they shouldn't trigger the loading indicator
const nonPersistentCallbacks = pendingCallbacks.filter(
cb => !cb.callback.persistent
);

const next = Boolean(nonPersistentCallbacks.length);

if (isLoading !== next) {
dispatch(setIsLoading(next));
Expand Down
1 change: 1 addition & 0 deletions dash/dash-renderer/src/types/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ICallbackDefinition {
running: any;
no_output?: boolean;
websocket?: boolean;
persistent?: boolean;
}

export interface ICallbackProperty {
Expand Down
Loading
Loading