From 1fc9b24eed35b4c21036f659b8d9d6ca8c36e2db Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:37:59 -0700 Subject: [PATCH 1/8] renderer: relax MATCH wildcard validation when Outputs lack MATCH Allow Input/State MATCH wildcards that aren't covered by the Output's MATCH keys when the callback has a fixed-id Output, no Output, or only ALL-wildcard Outputs. ALLSMALLER still requires a MATCH reference. Fixes #2462. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dash-renderer/src/actions/dependencies.js | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/dash/dash-renderer/src/actions/dependencies.js b/dash/dash-renderer/src/actions/dependencies.js index 2ce6d0d577..4d57bbb5f3 100644 --- a/dash/dash-renderer/src/actions/dependencies.js +++ b/dash/dash-renderer/src/actions/dependencies.js @@ -434,24 +434,37 @@ function findMismatchedWildcards(outputs, inputs, state, head, dispatchError) { ]); } }); + // When the Outputs don't carry any MATCH keys (fixed-id outputs, no + // outputs, or wildcard outputs with only ALL), the Inputs/State may use + // MATCH freely — each firing is identified by the triggering input's + // MATCH values. ALLSMALLER still requires a MATCH reference, so that + // case remains an error. See issue #2462. + const outputsHaveMatch = out0MatchKeys.length > 0; [ [inputs, 'Input'], [state, 'State'] ].forEach(([args, cls]) => { args.forEach((arg, i) => { const {matchKeys, allsmallerKeys} = findWildcardKeys(arg.id); - const allWildcardKeys = matchKeys.concat(allsmallerKeys); - const diff = difference(allWildcardKeys, out0MatchKeys); + const diffKeys = outputsHaveMatch + ? matchKeys.concat(allsmallerKeys) + : allsmallerKeys; + const diff = difference(diffKeys, out0MatchKeys); if (diff.length) { diff.sort(); + const outDesc = outputs.length + ? `Output 0 (${combineIdAndProp(outputs[0])})` + : 'the (absent) Output'; dispatchError('`Input` / `State` wildcards not in `Output`s', [ head, `${cls} ${i} (${combineIdAndProp(arg)})`, `has MATCH or ALLSMALLER on key(s) ${diff.join(', ')}`, - `where Output 0 (${combineIdAndProp(outputs[0])})`, + `where ${outDesc}`, 'does not have a MATCH wildcard. Inputs and State do not', - 'need every MATCH from the Output(s), but they cannot have', - 'extras beyond the Output(s).' + 'need every MATCH from the Output(s), but ALLSMALLER', + 'requires a matching MATCH in the Output(s), and when', + 'the Output(s) have any MATCH, Input/State MATCH keys', + 'must be a subset of them.' ]); } }); From 931078da2d97489657f85c6826cfda1b1613c47d Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:38:42 -0700 Subject: [PATCH 2/8] renderer: uniquify callback resolvedId per MATCH trigger When a callback's Outputs have no MATCH keys, multiple simultaneous MATCH-triggered firings would collapse to the same resolvedId and dedupe into a single invocation, losing all but the first trigger's resolver. Thread the triggering Input's MATCH values through as the callback's anyVals so each MATCH firing is addressable separately. Refs #2462. Co-Authored-By: Claude Opus 4.7 (1M context) --- dash/dash-renderer/src/actions/dependencies.js | 15 ++++++++++++--- dash/dash-renderer/src/actions/dependencies_ts.ts | 10 +++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dash/dash-renderer/src/actions/dependencies.js b/dash/dash-renderer/src/actions/dependencies.js index 4d57bbb5f3..7b5d1665f0 100644 --- a/dash/dash-renderer/src/actions/dependencies.js +++ b/dash/dash-renderer/src/actions/dependencies.js @@ -1056,7 +1056,7 @@ export function idMatch( return true; } -function getAnyVals(patternVals, vals) { +export function getAnyVals(patternVals, vals) { const matches = []; for (let i = 0; i < patternVals.length; i++) { if (patternVals[i] === MATCH) { @@ -1155,7 +1155,12 @@ function addResolvedFromOutputs(callback, outPattern, outs, matches) { }); } -export function addAllResolvedFromOutputs(resolve, paths, matches) { +export function addAllResolvedFromOutputs( + resolve, + paths, + matches, + triggerAnyVals = '' +) { return callback => { const {matchKeys, firstSingleOutput, outputs} = callback; if (matchKeys.length) { @@ -1192,7 +1197,11 @@ export function addAllResolvedFromOutputs(resolve, paths, matches) { }); } } else { - const cb = makeResolvedCallback(callback, resolve, ''); + // Outputs have no MATCH keys (fixed-id outputs or no output). + // Fall back to the triggering input's MATCH values so that + // separate MATCH triggers produce distinct resolvedIds and + // aren't deduplicated into a single firing. See issue #2462. + const cb = makeResolvedCallback(callback, resolve, triggerAnyVals); matches.push(cb); } }; diff --git a/dash/dash-renderer/src/actions/dependencies_ts.ts b/dash/dash-renderer/src/actions/dependencies_ts.ts index 75cbdc66dd..33f968cf91 100644 --- a/dash/dash-renderer/src/actions/dependencies_ts.ts +++ b/dash/dash-renderer/src/actions/dependencies_ts.ts @@ -25,6 +25,7 @@ import { } from '../types/callbacks'; import { addAllResolvedFromOutputs, + getAnyVals, getUnfilteredLayoutCallbacks, idMatch, isMultiValued, @@ -72,11 +73,18 @@ export function getCallbacksByInput( } patterns.forEach(pattern => { if (idMatch(_keys, vals, pattern.values)) { + // When a callback's Outputs have no MATCH keys, the + // triggering Input's MATCH values are what uniquify each + // firing's resolvedId (see addAllResolvedFromOutputs). + // Callbacks whose Outputs do carry MATCH keys ignore this + // value since the Output pattern drives resolution. + const triggerAnyVals = getAnyVals(pattern.values, vals); pattern.callbacks.forEach( addAllResolvedFromOutputs( resolveDeps(_keys, vals, pattern.values), paths, - matches + matches, + triggerAnyVals ) ); } From 224a4c1b975e0e8c7289115206f2225b15c0c0c7 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:41:58 -0700 Subject: [PATCH 3/8] tests(renderer): cover relaxed MATCH semantics and trigger resolvedId - findMismatchedWildcards permits MATCH in Input/State when the Output is a fixed-id, no-output, or ALL-only wildcard. - ALLSMALLER and cross-Output MATCH mismatches still error. - getAnyVals returns the trigger's MATCH values. - getCallbacksByInput yields distinct resolvedIds per MATCH trigger when the Output has no MATCH keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- dash/dash-renderer/tests/dependencies.test.js | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 dash/dash-renderer/tests/dependencies.test.js diff --git a/dash/dash-renderer/tests/dependencies.test.js b/dash/dash-renderer/tests/dependencies.test.js new file mode 100644 index 0000000000..c8ae7a9a84 --- /dev/null +++ b/dash/dash-renderer/tests/dependencies.test.js @@ -0,0 +1,250 @@ +import {expect} from 'chai'; +import {beforeEach, describe, it} from 'mocha'; +import { + computeGraphs, + getAnyVals +} from '../src/actions/dependencies'; +import {getCallbacksByInput} from '../src/actions/dependencies_ts'; +import {EventEmitter} from '../src/actions/utils'; + +const config = {validate_callbacks: true}; + +// Build a paths fixture that matches the layout crawling output +// (paths.strs for string ids, paths.objs for wildcard ids). +function makePaths(stringIds, wildcardItems) { + const paths = { + strs: {}, + objs: {}, + events: new EventEmitter() + }; + stringIds.forEach(id => { + paths.strs[id] = ['props', 'children', 0]; + }); + Object.entries(wildcardItems || {}).forEach(([keyStr, items]) => { + paths.objs[keyStr] = items.map((values, i) => ({ + values, + path: ['props', 'children', i] + })); + }); + return paths; +} + +describe('dependencies — MATCH validation (#2462)', () => { + let errors; + const dispatchError = (message, lines) => { + errors.push({message, lines}); + }; + + beforeEach(() => { + errors = []; + }); + + it('permits MATCH Input with fixed-id Output', () => { + computeGraphs( + [ + { + output: 'out.children', + inputs: [ + {id: '{"id":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: false + } + ], + dispatchError, + config + ); + expect(errors).to.eql([]); + }); + + it('permits MATCH Input with no-output callback', () => { + computeGraphs( + [ + { + output: '', + inputs: [ + {id: '{"id":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: true + } + ], + dispatchError, + config + ); + expect(errors).to.eql([]); + }); + + it('permits MATCH State with fixed-id Output', () => { + computeGraphs( + [ + { + output: 'out.children', + inputs: [ + {id: '{"id":["MATCH"]}', property: 'n_clicks'} + ], + state: [ + {id: '{"id":["MATCH"]}', property: 'id'} + ], + no_output: false + } + ], + dispatchError, + config + ); + expect(errors).to.eql([]); + }); + + it('permits MATCH Input with ALL-only wildcard Output', () => { + computeGraphs( + [ + { + output: '{"id":["ALL"]}.children', + inputs: [ + {id: '{"type":"btn","idx":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: false + } + ], + dispatchError, + config + ); + expect(errors).to.eql([]); + }); + + it('still errors on ALLSMALLER Input with fixed Output', () => { + computeGraphs( + [ + { + output: 'out.children', + inputs: [ + {id: '{"id":["ALLSMALLER"]}', property: 'value'} + ], + state: [], + no_output: false + } + ], + dispatchError, + config + ); + expect(errors).to.have.lengthOf(1); + expect(errors[0].message).to.equal( + '`Input` / `State` wildcards not in `Output`s' + ); + }); + + it('still errors when Output has MATCH on different keys than Input', () => { + computeGraphs( + [ + { + output: '{"a":["MATCH"]}.children', + inputs: [ + {id: '{"b":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: false + } + ], + dispatchError, + config + ); + // Should produce an error because out has MATCH on "a" + // but input has MATCH on "b". + expect(errors).to.have.lengthOf(1); + expect(errors[0].message).to.equal( + '`Input` / `State` wildcards not in `Output`s' + ); + }); + + it('still errors on Mismatched MATCH across Outputs', () => { + computeGraphs( + [ + { + output: '..{"b":["MATCH"]}.children...{"b":["ALL"],"c":1}.children..', + inputs: [ + {id: '{"b":["MATCH"],"c":2}', property: 'children'} + ], + state: [], + no_output: false + } + ], + dispatchError, + config + ); + const msgs = errors.map(e => e.message); + expect(msgs).to.include('Mismatched `MATCH` wildcards across `Output`s'); + }); +}); + +describe('dependencies — MATCH trigger resolvedId (#2462)', () => { + it('getAnyVals picks MATCH values from trigger id', () => { + // Use the same object reference for MATCH that the module uses + // internally by exercising computeGraphs first. + const errors = []; + const graphs = computeGraphs( + [ + { + output: 'out.children', + inputs: [ + {id: '{"id":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: false + } + ], + (m, l) => errors.push({m, l}), + config + ); + expect(errors).to.eql([]); + const pattern = graphs.inputPatterns.id.n_clicks[0]; + const anyVals = getAnyVals(pattern.values, ['btn-1']); + expect(anyVals).to.equal('["btn-1"]'); + }); + + it('fires distinct callbacks per MATCH trigger when Output is fixed', () => { + const errors = []; + const graphs = computeGraphs( + [ + { + output: 'out.children', + inputs: [ + {id: '{"id":["MATCH"]}', property: 'n_clicks'} + ], + state: [], + no_output: false + } + ], + (m, l) => errors.push({m, l}), + config + ); + expect(errors).to.eql([]); + + const paths = makePaths(['out'], { + id: [['btn-1'], ['btn-2']] + }); + + const first = getCallbacksByInput( + graphs, + paths, + {id: 'btn-1'}, + 'n_clicks', + undefined, + false + ); + const second = getCallbacksByInput( + graphs, + paths, + {id: 'btn-2'}, + 'n_clicks', + undefined, + false + ); + + expect(first).to.have.lengthOf(1); + expect(second).to.have.lengthOf(1); + expect(first[0].resolvedId).to.not.equal(second[0].resolvedId); + expect(first[0].resolvedId).to.include('btn-1'); + expect(second[0].resolvedId).to.include('btn-2'); + }); +}); From 5ed8ded1cb883927759bcedbcb1bb2f0e3e609e9 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:42:40 -0700 Subject: [PATCH 4/8] tests(integration): exercise MATCH Input with fixed/no Output Adds three integration tests covering the relaxed semantics from #2462: - MATCH Input + fixed-id Output + State MATCH returns the right id. - MATCH Input + no Output uses set_props against a fixed-id target. - Repeated MATCH clicks resolve State to the current trigger, not a stale first one (verifies per-trigger resolvedId uniqueness). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/callbacks/test_wildcards.py | 135 +++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 9fd7337040..659147eda5 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -6,7 +6,18 @@ from dash.testing import wait import dash -from dash import Dash, Input, Output, State, ALL, ALLSMALLER, MATCH, html, dcc +from dash import ( + Dash, + Input, + Output, + State, + ALL, + ALLSMALLER, + MATCH, + html, + dcc, + set_props, +) from tests.assets.todo_app import todo_app from tests.assets.grouping_app import grouping_app @@ -619,3 +630,125 @@ def on_click(_) -> str: assert not dash_duo.find_element("#buttons button:nth-child(2)").get_attribute( "disabled" ) + + +def test_cbwc009_match_input_fixed_output(dash_duo): + # Issue #2462: allow MATCH in Input with a fixed-id Output. + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button( + "Alpha", + id={"type": "btn", "index": "alpha"}, + ), + html.Button( + "Beta", + id={"type": "btn", "index": "beta"}, + ), + html.Div("initial", id="out"), + ] + ) + + @app.callback( + Output("out", "children"), + Input({"type": "btn", "index": MATCH}, "n_clicks"), + State({"type": "btn", "index": MATCH}, "id"), + prevent_initial_call=True, + ) + def show_clicked(_, id_): + return f"clicked {id_['index']}" + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#out", "initial") + + dash_duo.find_element( + '[id=\\{\\"index\\"\\:\\"alpha\\"\\,\\"type\\"\\:\\"btn\\"\\}]' + ).click() + dash_duo.wait_for_text_to_equal("#out", "clicked alpha") + + dash_duo.find_element( + '[id=\\{\\"index\\"\\:\\"beta\\"\\,\\"type\\"\\:\\"btn\\"\\}]' + ).click() + dash_duo.wait_for_text_to_equal("#out", "clicked beta") + + assert dash_duo.get_logs() == [] + + +def test_cbwc010_match_input_no_output(dash_duo): + # Issue #2462: allow MATCH in Input with no Output (set_props). + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button( + "One", + id={"type": "btn", "index": 1}, + ), + html.Button( + "Two", + id={"type": "btn", "index": 2}, + ), + html.Div("initial", id="out"), + ] + ) + + @app.callback( + Input({"type": "btn", "index": MATCH}, "n_clicks"), + State({"type": "btn", "index": MATCH}, "id"), + prevent_initial_call=True, + ) + def announce(_, id_): + set_props("out", {"children": f"clicked index={id_['index']}"}) + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#out", "initial") + + dash_duo.find_element( + '[id=\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"btn\\"\\}]' + ).click() + dash_duo.wait_for_text_to_equal("#out", "clicked index=1") + + dash_duo.find_element( + '[id=\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"btn\\"\\}]' + ).click() + dash_duo.wait_for_text_to_equal("#out", "clicked index=2") + + assert dash_duo.get_logs() == [] + + +def test_cbwc011_match_input_fixed_output_uses_state(dash_duo): + # Verifies the State-MATCH resolver uses the triggering input's MATCH + # value (not some stale first trigger), across repeated clicks. + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button("A", id={"role": "tab", "name": "a"}), + html.Button("B", id={"role": "tab", "name": "b"}), + html.Button("C", id={"role": "tab", "name": "c"}), + html.Pre("-", id="trail"), + ] + ) + + @app.callback( + Output("trail", "children"), + Input({"role": "tab", "name": MATCH}, "n_clicks"), + State({"role": "tab", "name": MATCH}, "id"), + State("trail", "children"), + prevent_initial_call=True, + ) + def append(_, id_, current): + prev = "" if current == "-" else current + return f"{prev}{id_['name']}" + + dash_duo.start_server(app) + + dash_duo.wait_for_text_to_equal("#trail", "-") + + for name in ["a", "b", "c", "a"]: + dash_duo.find_element( + f'[id=\\{{\\"name\\"\\:\\"{name}\\"\\,\\"role\\"\\:\\"tab\\"\\}}]' + ).click() + + dash_duo.wait_for_text_to_equal("#trail", "abca") + assert dash_duo.get_logs() == [] From cef944bd840ce4dba6a089e4a0ef7b7ce6727e64 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:43:55 -0700 Subject: [PATCH 5/8] tests(devtools): assert MATCH relaxation and ALLSMALLER strictness Adds test_dvcv017 to confirm that callbacks with fixed-id, no-output, or ALL-only wildcard outputs accept MATCH inputs without dispatching validation errors, and test_dvcv018 to confirm ALLSMALLER still needs a matching MATCH in the Output(s). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../devtools/test_callback_validation.py | 89 ++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index 59b874012e..ac298d3208 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -1,7 +1,18 @@ import flask import pytest -from dash import Dash, Input, Output, State, MATCH, ALL, ALLSMALLER, html, dcc +from dash import ( + Dash, + Input, + Output, + State, + MATCH, + ALL, + ALLSMALLER, + html, + dcc, + set_props, +) from dash.testing import wait debugging = dict( @@ -815,3 +826,79 @@ def c2(children): ] ] check_errors(dash_duo, specs) + + +def test_dvcv017_match_input_permitted_no_output_match(dash_duo): + # Issue #2462: when Outputs don't carry MATCH wildcards, Inputs/State + # may use MATCH freely. These callbacks should load without errors. + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button("btn", id={"type": "btn", "idx": 1}), + html.Div(id="out-a"), + html.Div(id="out-b"), + html.Div(id={"type": "wild", "idx": 1}), + html.Div(id={"type": "wild", "idx": 2}), + ] + ) + + # fixed Output + MATCH Input + @app.callback( + Output("out-a", "children"), + Input({"type": "btn", "idx": MATCH}, "n_clicks"), + prevent_initial_call=True, + ) + def a(_): + return "a" + + # no-Output + MATCH Input + @app.callback( + Input({"type": "btn", "idx": MATCH}, "n_clicks"), + prevent_initial_call=True, + ) + def b(_): + set_props("out-b", {"children": "b"}) + + # ALL-only wildcard Output + MATCH Input on a different key + @app.callback( + Output({"type": "wild", "idx": ALL}, "children"), + Input({"type": "btn", "idx": MATCH}, "n_clicks"), + prevent_initial_call=True, + ) + def c(_): + return ["c", "c"] + + dash_duo.start_server(app, **debugging) + + # All three callbacks should register without validation errors. + wait.until(lambda: ~dash_duo.redux_state_is_loading, 2) + dash_duo.wait_for_no_elements(dash_duo.devtools_error_count_locator) + assert dash_duo.get_logs() == [] + + +def test_dvcv018_allsmaller_still_errors_with_fixed_output(dash_duo): + # Issue #2462 leaves ALLSMALLER strict: it still requires a + # corresponding MATCH in the Output(s). + app = Dash(__name__) + app.layout = html.Div() + + @app.callback( + Output("out", "children"), + Input({"i": ALLSMALLER}, "value"), + ) + def x(_): + return "x" + + dash_duo.start_server(app, **debugging) + + specs = [ + [ + "`Input` / `State` wildcards not in `Output`s", + [ + 'Input 0 ({"i":ALLSMALLER}.value)', + "has MATCH or ALLSMALLER on key(s) i", + "Output 0 (out.children)", + ], + ], + ] + check_errors(dash_duo, specs) From 90540e5c9a991fbc8d53127d3e1eb83b6d7ff929 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:44:17 -0700 Subject: [PATCH 6/8] docs: changelog entry for #2462 MATCH relaxation Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962b384db7..53947090a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3690](https://github.com/plotly/dash/pull/3690) Fixes Input when min or max is set to None - [#3723](https://github.com/plotly/dash/pull/3723) Fix misaligned `dcc.Slider` marks when some labels are empty strings - [#3740](https://github.com/plotly/dash/pull/3740) Fix cannot tab into dropdowns in Safari +- [#2462](https://github.com/plotly/dash/issues/2462) Allow `MATCH` in `Input`/`State` when the callback's `Output` has no wildcards (fixed-id Output, no Output, or `ALL`-only wildcard Output). `ALLSMALLER` still requires a corresponding `MATCH` in an Output. ## [4.1.0] - 2026-03-23 From 29372932c953458c4af6f5444682cdb48008abe0 Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 10:47:31 -0700 Subject: [PATCH 7/8] tests: scope to the two #2462 cases per reviewer feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the extra MATCH-repeated-clicks integration test and the ALLSMALLER-still-errors devtools test; keep only the two new-case tests (MATCH → fixed Output, MATCH → no Output) plus the validation smoke test. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/callbacks/test_wildcards.py | 36 ------------------- .../devtools/test_callback_validation.py | 27 -------------- 2 files changed, 63 deletions(-) diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 659147eda5..82ef13c6bb 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -716,39 +716,3 @@ def announce(_, id_): assert dash_duo.get_logs() == [] - -def test_cbwc011_match_input_fixed_output_uses_state(dash_duo): - # Verifies the State-MATCH resolver uses the triggering input's MATCH - # value (not some stale first trigger), across repeated clicks. - app = Dash(__name__) - app.layout = html.Div( - [ - html.Button("A", id={"role": "tab", "name": "a"}), - html.Button("B", id={"role": "tab", "name": "b"}), - html.Button("C", id={"role": "tab", "name": "c"}), - html.Pre("-", id="trail"), - ] - ) - - @app.callback( - Output("trail", "children"), - Input({"role": "tab", "name": MATCH}, "n_clicks"), - State({"role": "tab", "name": MATCH}, "id"), - State("trail", "children"), - prevent_initial_call=True, - ) - def append(_, id_, current): - prev = "" if current == "-" else current - return f"{prev}{id_['name']}" - - dash_duo.start_server(app) - - dash_duo.wait_for_text_to_equal("#trail", "-") - - for name in ["a", "b", "c", "a"]: - dash_duo.find_element( - f'[id=\\{{\\"name\\"\\:\\"{name}\\"\\,\\"role\\"\\:\\"tab\\"\\}}]' - ).click() - - dash_duo.wait_for_text_to_equal("#trail", "abca") - assert dash_duo.get_logs() == [] diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index ac298d3208..f8e08ae6e2 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -875,30 +875,3 @@ def c(_): dash_duo.wait_for_no_elements(dash_duo.devtools_error_count_locator) assert dash_duo.get_logs() == [] - -def test_dvcv018_allsmaller_still_errors_with_fixed_output(dash_duo): - # Issue #2462 leaves ALLSMALLER strict: it still requires a - # corresponding MATCH in the Output(s). - app = Dash(__name__) - app.layout = html.Div() - - @app.callback( - Output("out", "children"), - Input({"i": ALLSMALLER}, "value"), - ) - def x(_): - return "x" - - dash_duo.start_server(app, **debugging) - - specs = [ - [ - "`Input` / `State` wildcards not in `Output`s", - [ - 'Input 0 ({"i":ALLSMALLER}.value)', - "has MATCH or ALLSMALLER on key(s) i", - "Output 0 (out.children)", - ], - ], - ] - check_errors(dash_duo, specs) From 0e0774feaa85838300906e9c92b2aba981479d4f Mon Sep 17 00:00:00 2001 From: Ben Postlethwaite Date: Tue, 21 Apr 2026 11:30:50 -0700 Subject: [PATCH 8/8] style: run black and prettier on the new #2462 tests Black: collapse two click() chains that fit on one line and drop a trailing blank line. Prettier: reformat the new Mocha test file to match renderer style. Flake8 W391 clears once the trailing blank lines go. Co-Authored-By: Claude Opus 4.7 (1M context) --- dash/dash-renderer/tests/dependencies.test.js | 46 +++++++------------ tests/integration/callbacks/test_wildcards.py | 9 +--- .../devtools/test_callback_validation.py | 1 - 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/dash/dash-renderer/tests/dependencies.test.js b/dash/dash-renderer/tests/dependencies.test.js index c8ae7a9a84..8051e4f010 100644 --- a/dash/dash-renderer/tests/dependencies.test.js +++ b/dash/dash-renderer/tests/dependencies.test.js @@ -1,9 +1,6 @@ import {expect} from 'chai'; import {beforeEach, describe, it} from 'mocha'; -import { - computeGraphs, - getAnyVals -} from '../src/actions/dependencies'; +import {computeGraphs, getAnyVals} from '../src/actions/dependencies'; import {getCallbacksByInput} from '../src/actions/dependencies_ts'; import {EventEmitter} from '../src/actions/utils'; @@ -44,9 +41,7 @@ describe('dependencies — MATCH validation (#2462)', () => { [ { output: 'out.children', - inputs: [ - {id: '{"id":["MATCH"]}', property: 'n_clicks'} - ], + inputs: [{id: '{"id":["MATCH"]}', property: 'n_clicks'}], state: [], no_output: false } @@ -62,9 +57,7 @@ describe('dependencies — MATCH validation (#2462)', () => { [ { output: '', - inputs: [ - {id: '{"id":["MATCH"]}', property: 'n_clicks'} - ], + inputs: [{id: '{"id":["MATCH"]}', property: 'n_clicks'}], state: [], no_output: true } @@ -80,12 +73,8 @@ describe('dependencies — MATCH validation (#2462)', () => { [ { output: 'out.children', - inputs: [ - {id: '{"id":["MATCH"]}', property: 'n_clicks'} - ], - state: [ - {id: '{"id":["MATCH"]}', property: 'id'} - ], + inputs: [{id: '{"id":["MATCH"]}', property: 'n_clicks'}], + state: [{id: '{"id":["MATCH"]}', property: 'id'}], no_output: false } ], @@ -101,7 +90,10 @@ describe('dependencies — MATCH validation (#2462)', () => { { output: '{"id":["ALL"]}.children', inputs: [ - {id: '{"type":"btn","idx":["MATCH"]}', property: 'n_clicks'} + { + id: '{"type":"btn","idx":["MATCH"]}', + property: 'n_clicks' + } ], state: [], no_output: false @@ -118,9 +110,7 @@ describe('dependencies — MATCH validation (#2462)', () => { [ { output: 'out.children', - inputs: [ - {id: '{"id":["ALLSMALLER"]}', property: 'value'} - ], + inputs: [{id: '{"id":["ALLSMALLER"]}', property: 'value'}], state: [], no_output: false } @@ -139,9 +129,7 @@ describe('dependencies — MATCH validation (#2462)', () => { [ { output: '{"a":["MATCH"]}.children', - inputs: [ - {id: '{"b":["MATCH"]}', property: 'n_clicks'} - ], + inputs: [{id: '{"b":["MATCH"]}', property: 'n_clicks'}], state: [], no_output: false } @@ -173,7 +161,9 @@ describe('dependencies — MATCH validation (#2462)', () => { config ); const msgs = errors.map(e => e.message); - expect(msgs).to.include('Mismatched `MATCH` wildcards across `Output`s'); + expect(msgs).to.include( + 'Mismatched `MATCH` wildcards across `Output`s' + ); }); }); @@ -186,9 +176,7 @@ describe('dependencies — MATCH trigger resolvedId (#2462)', () => { [ { output: 'out.children', - inputs: [ - {id: '{"id":["MATCH"]}', property: 'n_clicks'} - ], + inputs: [{id: '{"id":["MATCH"]}', property: 'n_clicks'}], state: [], no_output: false } @@ -208,9 +196,7 @@ describe('dependencies — MATCH trigger resolvedId (#2462)', () => { [ { output: 'out.children', - inputs: [ - {id: '{"id":["MATCH"]}', property: 'n_clicks'} - ], + inputs: [{id: '{"id":["MATCH"]}', property: 'n_clicks'}], state: [], no_output: false } diff --git a/tests/integration/callbacks/test_wildcards.py b/tests/integration/callbacks/test_wildcards.py index 82ef13c6bb..c151e8c277 100644 --- a/tests/integration/callbacks/test_wildcards.py +++ b/tests/integration/callbacks/test_wildcards.py @@ -704,15 +704,10 @@ def announce(_, id_): dash_duo.wait_for_text_to_equal("#out", "initial") - dash_duo.find_element( - '[id=\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"btn\\"\\}]' - ).click() + dash_duo.find_element('[id=\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"btn\\"\\}]').click() dash_duo.wait_for_text_to_equal("#out", "clicked index=1") - dash_duo.find_element( - '[id=\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"btn\\"\\}]' - ).click() + dash_duo.find_element('[id=\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"btn\\"\\}]').click() dash_duo.wait_for_text_to_equal("#out", "clicked index=2") assert dash_duo.get_logs() == [] - diff --git a/tests/integration/devtools/test_callback_validation.py b/tests/integration/devtools/test_callback_validation.py index f8e08ae6e2..eaee814980 100644 --- a/tests/integration/devtools/test_callback_validation.py +++ b/tests/integration/devtools/test_callback_validation.py @@ -874,4 +874,3 @@ def c(_): wait.until(lambda: ~dash_duo.redux_state_is_loading, 2) dash_duo.wait_for_no_elements(dash_duo.devtools_error_count_locator) assert dash_duo.get_logs() == [] -