') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Allow accessing a Dag's members via `[]` by Dev-iL · Pull Request #65586 · apache/airflow · GitHub
Skip to content

Allow accessing a Dag's members via [] - #65586

Merged
potiuk merged 3 commits into
apache:mainfrom
Dev-iL:2604/dag_getitem
Apr 28, 2026
Merged

Allow accessing a Dag's members via []#65586
potiuk merged 3 commits into
apache:mainfrom
Dev-iL:2604/dag_getitem

Conversation

@Dev-iL

@Dev-iLDev-iL commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

related: #64430

Summary

  • Add dag[id] subscript syntax to DAG and SerializedDAG for accessing any node (task or task group) by its fully-qualified ID.
  • Update TaskGroup.__getitem__ to raise TaskItemNotFound (instead of a raw KeyError) on a miss, so callers can catch either KeyError or TaskNotFound.
  • Introduce TaskItemNotFound(TaskNotFound, KeyError) as a new exception in airflow.sdk.exceptions (and re-export it from airflow.exceptions) — a KeyError subtype that also satisfies TaskNotFound, giving callers flexibility without breaking existing KeyError handlers.

Details

ComponentChange
task-sdk/src/airflow/sdk/exceptions.pyAdd TaskItemNotFound(TaskNotFound, KeyError)
airflow-core/src/airflow/exceptions.pyRe-export TaskItemNotFound; add fallback stub
task-sdk/src/airflow/sdk/definitions/dag.pyAdd DAG.__getitem__: checks task_dict then task_group_dict, returns DAGNode
task-sdk/src/airflow/sdk/definitions/taskgroup.pyUpdate TaskGroup.__getitem__ to re-raise KeyError as TaskItemNotFound
airflow-core/src/airflow/serialization/definitions/dag.pyAdd SerializedDAG.__getitem__: same lookup logic, returns SerializedOperator | SerializedTaskGroup

Usage

withDAG("my_dag", ...) asdag:
withTaskGroup("section") astg:
t=BashOperator(task_id="my_task", bash_command="echo 1")
# Access a top-level taskassertdag["my_task"] ist# hypothetical top-level task# Access a grouped task by fully-qualified IDassertdag["section.my_task"] ist# Access the task group itselfassertdag["section"] istg# Chain subscripts (DAG returns TaskGroup, TaskGroup returns child by label)assertdag["section"]["my_task"] ist# All misses raise TaskItemNotFound (catchable as KeyError or TaskNotFound)dag["nonexistent"] # raises TaskItemNotFound

Two access patterns for nested nodes

dag[id] supports two equivalent ways to reach a task inside a group:

PatternMechanism
dag["section.my_task"]Direct flat lookup in task_dict — O(1)
dag["section"]["my_task"]dag["section"] returns the TaskGroup; tg["my_task"] resolves by label

Both are tested. The flat lookup is marginally cheaper; the chained form is more readable when the group is already in scope.

TaskItemNotFound exception hierarchy

KeyError
└── TaskItemNotFound
└── (also) TaskNotFound ← AirflowException

Callers can catch it as KeyError (existing dict-style handlers), TaskNotFound (Airflow task-lookup handlers), or TaskItemNotFound (subscript-specific handlers).

The fallback stub in airflow.exceptions (active when airflow.sdk is not installed) mirrors the same __str__ override so str(exc) never includes the extra quotes that KeyError.__str__ adds.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Sonnet 4.6 following the guidelines


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Comment threadairflow-core/src/airflow/serialization/definitions/dag.py Outdated
@Dev-iL
Dev-iLforce-pushed the 2604/dag_getitem branch 2 times, most recently from 14d741b to 53d7922CompareApril 23, 2026 05:28
@eladkaleladkal added this to the Airflow 3.2.2 milestone Apr 23, 2026
@Dev-iL

Dev-iL commented Apr 23, 2026

Copy link
Copy Markdown
CollaboratorAuthor

I'm considering renaming TaskItemNotFound to DagNodeNotFound (or simply NodeNotFound). The TaskItem name came from the method that raises it __getitem__ + the fact it wraps TaskNotFound.

I personally think {Dag}Node is better since it also covers task groups.

@ashb

ashb commented Apr 23, 2026

Copy link
Copy Markdown
Member

This is not a bug fix, its a new feature, so we usually shouldn't backport this sort of change.

@Dev-iLDev-iL added the ready for maintainer review Set after triaging when all criteria pass. label Apr 23, 2026
@ashb

ashb commented Apr 23, 2026

Copy link
Copy Markdown
Member

I'm considering renaming TaskItemNotFound to DagNodeNotFound (or simply NodeNotFound).

Yes, I think either of those two names are better

- introduce `TaskItemNotFound` (a `KeyError` subtype) to the SDK exceptions module.
- Enable `dag[task_id]` syntax, raising `TaskItemNotFound` on miss.
- Add `SerializedDAG.__getitem__` for consistency.
`dag[id]` now searches task_dict then task_group_dict, so both tasks and groups are reachable by their fully-qualified ID. SerializedDAG follows the same logic. Both chained access (`dag["group"]["task"]`) and qualified-id access (`dag["group.task"]`) are covered by new tests.
@Subham-KRLX

Copy link
Copy Markdown
Contributor

LGTM Implementation is correct as per my knowledge follows the same pattern as #64430 Exception hierarchy works as intended, dual access patterns both function properly.

@Dev-iL

Copy link
Copy Markdown
CollaboratorAuthor

LGTM Implementation is correct as per my knowledge follows the same pattern as #64430 Exception hierarchy works as intended, dual access patterns both function properly.

Thank you for taking a look! BTW it's possible to signal one's approval via the "Files changed" tab:

image

@Subham-KRLXSubham-KRLX left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@potiuk
potiuk merged commit d4d83ac into apache:mainApr 28, 2026
111 checks passed
@Dev-iL
Dev-iL deleted the 2604/dag_getitem branch April 28, 2026 07:52
@Dev-iL

Copy link
Copy Markdown
CollaboratorAuthor

@potiuk should we change #64430 to 3.3.0 too?

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:DAG-processingarea:task-sdkready for maintainer reviewSet after triaging when all criteria pass.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants

@Dev-iL@ashb@Subham-KRLX@potiuk@uranusjr@eladkal