This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 4436ef91d19 [v3-1-test] grid merge node dict storage (#61656) (#61789)
4436ef91d19 is described below
commit 4436ef91d199bee0fc93a267c9b6a74f2dde31ee
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Feb 12 10:54:28 2026 +0100
[v3-1-test] grid merge node dict storage (#61656) (#61789)
(cherry picked from commit 9da4a153b22854df7f9dd98cda093d33abd2488c)
Co-authored-by: Steve Ahn <[email protected]>
---
.../api_fastapi/core_api/services/ui/grid.py | 17 ++---
.../api_fastapi/core_api/services/ui/__init__.py | 16 +++++
.../api_fastapi/core_api/services/ui/test_grid.py | 72 ++++++++++++++++++++++
3 files changed, 94 insertions(+), 11 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
index 68520371116..b2e9b01e248 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
@@ -33,22 +33,17 @@ log = structlog.get_logger(logger_name=__name__)
def _merge_node_dicts(current, new) -> None:
- current_ids = {node["id"] for node in current}
+ current_nodes_by_id = {node["id"]: node for node in current}
for node in new:
- if node["id"] in current_ids:
- current_node = _get_node_by_id(current, node["id"])
+ node_id = node["id"]
+ current_node = current_nodes_by_id.get(node_id)
+ if current_node is not None:
# if we have children, merge those as well
if current_node.get("children"):
- _merge_node_dicts(current_node["children"], node["children"])
+ _merge_node_dicts(current_node["children"],
node.get("children", []))
else:
current.append(node)
-
-
-def _get_node_by_id(nodes, node_id):
- for node in nodes:
- if node["id"] == node_id:
- return node
- return {}
+ current_nodes_by_id[node_id] = node
def agg_state(states):
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py
new file mode 100644
index 00000000000..13a83393a91
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py
new file mode 100644
index 00000000000..55c3532c171
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_grid.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from airflow.api_fastapi.core_api.services.ui.grid import _merge_node_dicts
+
+
+def test_merge_node_dicts_merges_children_and_appends_new_nodes():
+ current = [
+ {
+ "id": "group",
+ "label": "group",
+ "children": [{"id": "group.task_a", "label": "task_a"}],
+ },
+ {"id": "task", "label": "task"},
+ ]
+ new = [
+ {
+ "id": "group",
+ "label": "group",
+ "children": [{"id": "group.task_b", "label": "task_b"}],
+ },
+ {"id": "new_task", "label": "new_task"},
+ ]
+
+ _merge_node_dicts(current, new)
+
+ assert [node["id"] for node in current] == ["group", "task", "new_task"]
+ group_children = {child["id"] for child in current[0]["children"]}
+ assert group_children == {"group.task_a", "group.task_b"}
+
+
+def test_merge_node_dicts_preserves_existing_non_group_node_shape():
+ current = [{"id": "task", "label": "task"}]
+ new = [{"id": "task", "label": "task", "children": [{"id": "task.subtask",
"label": "subtask"}]}]
+
+ _merge_node_dicts(current, new)
+
+ assert current == [{"id": "task", "label": "task"}]
+
+
+def test_merge_node_dicts_large_merge_keeps_unique_nodes():
+ current = [{"id": f"group_{i}", "children": [{"id":
f"group_{i}.old_task"}]} for i in range(400)]
+ new = [{"id": f"group_{i}", "children": [{"id": f"group_{i}.new_task"}]}
for i in range(400)]
+ new.extend({"id": f"new_task_{i}"} for i in range(400))
+
+ _merge_node_dicts(current, new)
+
+ assert len(current) == 800
+ assert {child["id"] for child in current[0]["children"]} == {
+ "group_0.old_task",
+ "group_0.new_task",
+ }
+ assert {child["id"] for child in current[-401]["children"]} == {
+ "group_399.old_task",
+ "group_399.new_task",
+ }