Skip to content

Commit 11d7d36

Browse files
committed
Merge branch 'v4' into feature/dcc-refactor-input
2 parents 4a81cbe + 36c7171 commit 11d7d36

File tree

16 files changed

+331
-12289
lines changed

16 files changed

+331
-12289
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Added
8+
- [#3395](https://github.com/plotly/dash/pull/3396) Add position argument to hooks.devtool
9+
- [#3403](https://github.com/plotly/dash/pull/3403) Add app_context to get_app, allowing to get the current app in routes.
10+
- [#3407](https://github.com/plotly/dash/pull/3407) Add `hidden` to callback arguments, hiding the callback from appearing in the devtool callback graph.
11+
12+
## Fixed
13+
- [#3395](https://github.com/plotly/dash/pull/3395) Fix Components added through set_props() cannot trigger related callback functions. Fix [#3316](https://github.com/plotly/dash/issues/3316)
14+
- [#3397](https://github.com/plotly/dash/pull/3397) Add optional callbacks, suppressing callback warning for missing component ids for a single callback.
15+
516
## [3.2.0] - 2025-07-31
617

718
## Added

components/dash-html-components/package-lock.json

Lines changed: 1 addition & 5781 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/dash-table/package-lock.json

Lines changed: 0 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash/_callback.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def _invoke_callback(func, *args, **kwargs): # used to mark the frame for the d
6464
GLOBAL_INLINE_SCRIPTS = []
6565

6666

67-
# pylint: disable=too-many-locals
67+
# pylint: disable=too-many-locals,too-many-arguments
6868
def callback(
6969
*_args,
7070
background: bool = False,
@@ -77,6 +77,8 @@ def callback(
7777
cache_args_to_ignore: Optional[list] = None,
7878
cache_ignore_triggered=True,
7979
on_error: Optional[Callable[[Exception], Any]] = None,
80+
optional: Optional[bool] = False,
81+
hidden: Optional[bool] = False,
8082
**_kwargs,
8183
) -> Callable[..., Any]:
8284
"""
@@ -159,6 +161,10 @@ def callback(
159161
Function to call when the callback raises an exception. Receives the
160162
exception object as first argument. The callback_context can be used
161163
to access the original callback inputs, states and output.
164+
:param optional:
165+
Mark all dependencies as not required on the initial layout checks.
166+
:param hidden:
167+
Hide the callback from the devtools callbacks tab.
162168
"""
163169

164170
background_spec = None
@@ -213,6 +219,8 @@ def callback(
213219
manager=manager,
214220
running=running,
215221
on_error=on_error,
222+
optional=optional,
223+
hidden=hidden,
216224
)
217225

218226

@@ -258,6 +266,8 @@ def insert_callback(
258266
running=None,
259267
dynamic_creator: Optional[bool] = False,
260268
no_output=False,
269+
optional=False,
270+
hidden=False,
261271
):
262272
if prevent_initial_call is None:
263273
prevent_initial_call = config_prevent_initial_callbacks
@@ -281,6 +291,8 @@ def insert_callback(
281291
},
282292
"dynamic_creator": dynamic_creator,
283293
"no_output": no_output,
294+
"optional": optional,
295+
"hidden": hidden,
284296
}
285297
if running:
286298
callback_spec["running"] = running
@@ -624,6 +636,8 @@ def register_callback(
624636
dynamic_creator=allow_dynamic_callbacks,
625637
running=running,
626638
no_output=not has_output,
639+
optional=_kwargs.get("optional", False),
640+
hidden=_kwargs.get("hidden", False),
627641
)
628642

629643
# pylint: disable=too-many-locals

dash/_get_app.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
1+
import functools
2+
3+
from contextvars import ContextVar, copy_context
14
from textwrap import dedent
25

36
APP = None
47

8+
app_context = ContextVar("dash_app_context")
9+
10+
11+
def with_app_context(func):
12+
@functools.wraps(func)
13+
def wrap(self, *args, **kwargs):
14+
app_context.set(self)
15+
ctx = copy_context()
16+
return ctx.run(func, self, *args, **kwargs)
17+
18+
return wrap
19+
20+
21+
def with_app_context_async(func):
22+
@functools.wraps(func)
23+
async def wrap(self, *args, **kwargs):
24+
app_context.set(self)
25+
ctx = copy_context()
26+
print("copied and set")
27+
return await ctx.run(func, self, *args, **kwargs)
28+
29+
return wrap
30+
31+
32+
def with_app_context_factory(func, app):
33+
@functools.wraps(func)
34+
def wrap(*args, **kwargs):
35+
app_context.set(app)
36+
ctx = copy_context()
37+
return ctx.run(func, *args, **kwargs)
38+
39+
return wrap
40+
541

642
def get_app():
43+
try:
44+
ctx_app = app_context.get()
45+
if ctx_app is not None:
46+
return ctx_app
47+
except LookupError:
48+
pass
49+
750
if APP is None:
851
raise Exception(
952
dedent(
1053
"""
1154
App object is not yet defined. `app = dash.Dash()` needs to be run
12-
before `dash.get_app()` is called and can only be used within apps that use
13-
the `pages` multi-page app feature: `dash.Dash(use_pages=True)`.
55+
before `dash.get_app()`.
1456
1557
`dash.get_app()` is used to get around circular import issues when Python files
1658
within the pages/` folder need to reference the `app` object.

dash/_hooks.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323

2424
HookDataType = _tx.TypeVar("HookDataType")
25+
DevtoolPosition = _tx.Literal["right", "left"]
2526

2627

2728
# pylint: disable=too-few-public-methods
@@ -217,7 +218,13 @@ def wrap(func: _t.Callable[[_t.Dict], _t.Any]):
217218

218219
return wrap
219220

220-
def devtool(self, namespace: str, component_type: str, props=None):
221+
def devtool(
222+
self,
223+
namespace: str,
224+
component_type: str,
225+
props=None,
226+
position: DevtoolPosition = "right",
227+
):
221228
"""
222229
Add a component to be rendered inside the dev tools.
223230
@@ -232,6 +239,7 @@ def devtool(self, namespace: str, component_type: str, props=None):
232239
"namespace": namespace,
233240
"type": component_type,
234241
"props": props or {},
242+
"position": position,
235243
}
236244
)
237245

dash/dash-renderer/package-lock.json

Lines changed: 8 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dash/dash-renderer/src/actions/callbacks.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ import {
3636
} from '../types/callbacks';
3737
import {isMultiValued, stringifyId, isMultiOutputProp} from './dependencies';
3838
import {urlBase} from './utils';
39-
import {getCSRFHeader, dispatchError} from '.';
39+
import {getCSRFHeader, dispatchError, setPaths} from '.';
4040
import {createAction, Action} from 'redux-actions';
4141
import {addHttpHeaders} from '../actions';
4242
import {notifyObservers, updateProps} from './index';
4343
import {CallbackJobPayload} from '../reducers/callbackJobs';
4444
import {handlePatch, isPatch} from './patch';
45-
import {getPath} from './paths';
45+
import {computePaths, getPath} from './paths';
4646

4747
import {requestDependencies} from './requestDependencies';
4848

@@ -51,6 +51,7 @@ import {loadLibrary} from '../utils/libraries';
5151
import {parsePMCId} from './patternMatching';
5252
import {replacePMC} from './patternMatching';
5353
import {loaded, loading} from './loading';
54+
import {getComponentLayout} from '../wrapper/wrapping';
5455

5556
export const addBlockedCallbacks = createAction<IBlockedCallback[]>(
5657
CallbackActionType.AddBlocked
@@ -409,7 +410,30 @@ function sideUpdate(outputs: SideUpdateOutput, cb: ICallbackPayload) {
409410
return acc;
410411
}, [] as any[])
411412
.forEach(([id, idProps]) => {
413+
const state = getState();
412414
dispatch(updateComponent(id, idProps, cb));
415+
416+
const componentPath = getPath(state.paths, id);
417+
if (!componentPath) {
418+
// Component doesn't exist, doesn't matter just allow the
419+
// callback to continue.
420+
return;
421+
}
422+
const oldComponent = getComponentLayout(componentPath, state);
423+
424+
dispatch(
425+
setPaths(
426+
computePaths(
427+
{
428+
...oldComponent,
429+
props: {...oldComponent.props, ...idProps}
430+
},
431+
[...componentPath],
432+
state.paths,
433+
state.paths.events
434+
)
435+
)
436+
);
413437
});
414438
};
415439
}

dash/dash-renderer/src/actions/dependencies.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -570,12 +570,18 @@ export function validateCallbacksToLayout(state_, dispatchError) {
570570
for (const id in map) {
571571
const idProps = map[id];
572572
const fcb = flatten(values(idProps));
573-
const optional = all(
574-
({allow_optional}) => allow_optional,
575-
flatten(
576-
fcb.map(cb => concat(cb.outputs, cb.inputs, cb.states))
577-
).filter(dep => dep.id === id)
578-
);
573+
const optional = fcb.reduce((acc, cb) => {
574+
if (acc === false || cb.optional) {
575+
return acc;
576+
}
577+
const deps = concat(cb.outputs, cb.inputs, cb.states).filter(
578+
dep => dep.id === id
579+
);
580+
return (
581+
!deps.length ||
582+
all(({allow_optional}) => allow_optional, deps)
583+
);
584+
}, true);
579585
if (optional) {
580586
continue;
581587
}

dash/dash-renderer/src/components/error/CallbackGraph/CallbackGraphContainer.react.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ function generateElements(graphs, profile, extraLinks) {
7878
});
7979
}
8080

81-
(graphs.callbacks || []).forEach((callback, i) => {
81+
(graphs.callbacks || []).reduce((visibleIndex, callback) => {
82+
if (callback.hidden) {
83+
return visibleIndex;
84+
}
85+
8286
const cb = `__dash_callback__.${callback.output}`;
8387
const cbProfile = profile.callbacks[callback.output] || {};
8488
const count = cbProfile.count || 0;
@@ -87,7 +91,7 @@ function generateElements(graphs, profile, extraLinks) {
8791
elements.push({
8892
data: {
8993
id: cb,
90-
label: `callback.${i}`,
94+
label: `callback.${visibleIndex}`,
9195
type: 'callback',
9296
mode: callback.clientside_function ? 'client' : 'server',
9397
count: count,
@@ -97,21 +101,23 @@ function generateElements(graphs, profile, extraLinks) {
97101
}
98102
});
99103

100-
callback.outputs.map(({id, property}) => {
104+
callback.outputs.forEach(({id, property}) => {
101105
const nodeId = recordNode(id, property);
102106
recordEdge(cb, nodeId, 'output');
103107
});
104108

105-
callback.inputs.map(({id, property}) => {
109+
callback.inputs.forEach(({id, property}) => {
106110
const nodeId = recordNode(id, property);
107111
recordEdge(nodeId, cb, 'input');
108112
});
109113

110-
callback.state.map(({id, property}) => {
114+
callback.state.forEach(({id, property}) => {
111115
const nodeId = recordNode(id, property);
112116
recordEdge(nodeId, cb, 'state');
113117
});
114-
});
118+
119+
return visibleIndex + 1;
120+
}, 0);
115121

116122
// pull together props in the same component
117123
if (extraLinks) {

0 commit comments

Comments
 (0)