Feature: add partial pattern matching (partial=True) for callbacks - #3765
Feature: add partial pattern matching (partial=True) for callbacks#3765i-murray wants to merge 9 commits into
Conversation
Allow pattern-matching callbacks to match components whose IDs contain a superset of the pattern's keys. Opt-in per dependency via partial=True on Input, Output, and State. - Python: partial flag on DashDependency, subset-aware _id_matches() - JS/TS: addPartialPattern, partialIdMatch, resolvePartialDeps, updated getWatchedKeys, getCallbacksByInput, getCallbackByOutput, getUnfilteredLayoutCallbacks, resolveDeps, isMultiValued - Literal-only partial patterns are implicitly multi-valued - hasPartialPatterns guard for zero-cost when unused - 16 unit tests + 4 integration tests
|
If, instead of Additionally, if this feature is integrated, the relevant Dash documentation Pattern-Matching Callbacks would need to be updated. |
|
I like this feature, but yea |
|
@i-murray are you still interested in finishing this PR? |
b589d31 to
9b74c34
Compare
|
@camdecoster this branch should be ready for a review. I've just renamed the keyword argument from |
|
T4rk1n
left a comment
There was a problem hiding this comment.
The perf section's "zero cost when unused" doesn't hold — some of the new work runs even when no callback sets partial_pattern, and a single partial callback flips the global flag so the extra loops then run for all wildcard callbacks. Three spots below to tighten before merge.
| handleOneId(id, graphs.outputMap[id], graphs.inputMap[id]); | ||
| } else { | ||
| const keyStr = Object.keys(id).sort().join(','); | ||
| const _keys = Object.keys(id).sort(); |
There was a problem hiding this comment.
Second sort of the same keys — keyStr above already did Object.keys(id).sort(). And _keys is only read inside the hasPartialPatterns blocks, but it runs for every component regardless. Compute the sorted array once and build keyStr from it.
| @@ -1237,15 +1460,45 @@ export function getWatchedKeys(id, newProps, graphs) { | |||
| const vals = props(keys, id); | |||
| const keyStr = keys.join(','); | |||
| const keyPatterns = graphs.inputPatterns[keyStr]; | |||
There was a problem hiding this comment.
Lost the early return when there are no keyPatterns, so we now run the newProps.filter(...) even when there's nothing to match. Bail early when there are no keyPatterns and no partial patterns; only enter the per-prop loop when there's something to check.
| }); | ||
| outputPatterns = offloadPatterns(outputPatternMap, outputPatterns); | ||
| inputPatterns = offloadPatterns(inputPatternMap, inputPatterns); | ||
| hasPartialPatterns = parsedDependencies.some( |
There was a problem hiding this comment.
The loops in getWatchedKeys / getCallbacksByInput / getCallbackByOutput / getUnfilteredLayoutCallbacks walk every keyStr and only find out it's not partial after the split + subset check. Build partialInputPatterns / partialOutputPatterns here in the same O(D) pass so those loops scan only the partial keyStrs.



Feature: Partial Pattern Matching (
partial=True) for CallbacksDescription
Dash's pattern-matching callbacks currently require the pattern and component IDs to have exactly the same set of keys. This means a pattern like
{"type": "btn"}will not match a component with ID{"type": "btn", "index": 1}— even though the component clearly has the key and value the pattern cares about.This PR adds an opt-in
partial=Trueflag onInput,Output, andStatethat relaxes the key-matching constraint: a pattern's keys only need to be a subset of the component's keys. Extra keys on the component are ignored.Closes #3764
Usage
This fires for any component whose ID contains
"type": "btn", regardless of other keys:{"type": "btn", "index": 1}{"type": "btn", "page": "home", "section": "main"}{"type": "btn", "index": 2, "tab": "first"}Works with wildcards:
Design choices
Input/Output/Stateindependently opts in, so partial and non-partial deps can coexist in the same callback.{"type": "btn"}withpartial=Truecan match multiple components, so the callback receives a list (same asALL). Patterns withMATCHstill resolve one-at-a-time.hasPartialPatternsboolean guard on the callback graph ensures apps withoutpartial=Truepay no additional overhead. All new loops are skipped entirely.Contributor Checklist
partialattribute toDashDependency,Input,Output,State(Python)_id_matches()for subset key matching whenpartial=Truepartialflag viato_dict()and propagate throughinsert_callback()addPartialPattern()andpartialIdMatch()to frontend (dependencies.js)computeGraphs()to detect partial flags and sethasPartialPatternsgetWatchedKeys(),getCallbackByOutput(),getUnfilteredLayoutCallbacks()with partial pattern loopsresolvePartialDeps()and updategetCallbacksByInput()independencies_ts.tsisMultiValued()for implicit multi-valued partial patternsrequestedCallbacks.tstype compatibility for updatedisMultiValuedsignaturehasPartialPatternsguard to all new loops for zero-cost when unusedtests/unit/test_partial_matching.py— subset matching, wildcards, serialization, bidirectional checks, negative casestests/integration/callbacks/test_partial_wildcards.py— basic partial match, ALL collection, literal filtering, mixed key setsOptionals
CHANGELOG.mdPerformance
Partial matching requires cross-keyStr searches in several hot-path functions that were previously$O(1)$ hash lookups:
getWatchedKeys()getCallbacksByInput()getCallbackByOutput()getUnfilteredLayoutCallbacks()Where$K$ = number of distinct keyStr entries in the pattern index, $p$ = number of keys in the pattern (typically 1–5), and $N$ = number of components in a layout chunk.
hasPartialPatternsguardTo ensure zero cost for apps that don't use$O(D)$ time (where $D$ = total dependencies). Every new loop is guarded:
partial=True,computeGraphs()sets ahasPartialPatternsboolean on the graph in