Skip to content

Commit e062f54

Browse files
fhausmannskshetry
andauthored
Add option to collapse foreach/matrix stages into a single stage (#10945)
Co-authored-by: skshetry <18718008+skshetry@users.noreply.github.com>
1 parent 3c77443 commit e062f54

2 files changed

Lines changed: 121 additions & 3 deletions

File tree

dvc/commands/dag.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,80 @@ def _filter(graph, targets, full):
127127
return new_graph
128128

129129

130-
def _build(repo, target=None, full=False, outs=False):
130+
def _is_foreach_matrix_stage(node, join_string):
131+
if node.endswith(".dvc"):
132+
return False
133+
return join_string in node
134+
135+
136+
def _collapse_foreach_matrix_get_nodes(graph):
137+
from dvc.parsing import JOIN
138+
139+
new_nodes = set()
140+
nodes_to_remove = set()
141+
for _node in list(graph.nodes):
142+
if not _is_foreach_matrix_stage(_node, JOIN):
143+
continue
144+
nodes_to_remove.add(_node)
145+
new_nodes.add(_node.split(JOIN)[0])
146+
return new_nodes, nodes_to_remove
147+
148+
149+
def _collapse_foreach_matrix_get_edges(graph):
150+
from dvc.parsing import JOIN
151+
152+
new_edges = set()
153+
edges_to_remove = set()
154+
for _e1, _e2 in list(graph.edges):
155+
_replace = False
156+
_new_e1 = _e1
157+
_new_e2 = _e2
158+
if _is_foreach_matrix_stage(_e1, JOIN):
159+
_new_e1 = _e1.split(JOIN)[0]
160+
_replace = True
161+
if _is_foreach_matrix_stage(_e2, JOIN):
162+
_new_e2 = _e2.split(JOIN)[0]
163+
_replace = True
164+
if _replace:
165+
edges_to_remove.add((_e1, _e2))
166+
new_edges.add((_new_e1, _new_e2))
167+
return new_edges, edges_to_remove
168+
169+
170+
def _collapse_foreach_matrix(graph):
171+
new_nodes, nodes_to_remove = _collapse_foreach_matrix_get_nodes(graph)
172+
new_edges, edges_to_remove = _collapse_foreach_matrix_get_edges(graph)
173+
new_graph = graph.copy()
174+
new_graph.remove_edges_from(edges_to_remove)
175+
new_graph.add_nodes_from(new_nodes)
176+
new_graph.add_edges_from(new_edges)
177+
new_graph.remove_nodes_from(nodes_to_remove)
178+
return new_graph
179+
180+
181+
def _build(repo, target=None, full=False, outs=False, collapse_foreach_matrix=False):
131182
targets = _collect_targets(repo, target, outs)
132183
graph = _transform(repo.index, outs)
133-
return _filter(graph, targets, full)
184+
filtered_graph = _filter(graph, targets, full)
185+
if collapse_foreach_matrix:
186+
filtered_graph = _collapse_foreach_matrix(filtered_graph)
187+
return filtered_graph
134188

135189

136190
class CmdDAG(CmdBase):
137191
def run(self):
192+
from dvc.exceptions import InvalidArgumentError
193+
194+
if self.args.outs and self.args.collapse_foreach_matrix:
195+
raise InvalidArgumentError(
196+
"`--outs` and `--collapse-foreach-matrix` are mutually exclusive"
197+
)
138198
graph = _build(
139199
self.repo,
140200
target=self.args.target,
141201
full=self.args.full,
142202
outs=self.args.outs,
203+
collapse_foreach_matrix=self.args.collapse_foreach_matrix,
143204
)
144205

145206
if self.args.dot:
@@ -181,6 +242,12 @@ def add_parser(subparsers, parent_parser):
181242
dest="markdown",
182243
help="Print DAG with mermaid format wrapped in Markdown block.",
183244
)
245+
dag_parser.add_argument(
246+
"--collapse-foreach-matrix",
247+
action="store_true",
248+
default=False,
249+
help="Collapse foreach/matrix stages to single stage.",
250+
)
184251
dag_parser.add_argument(
185252
"--full",
186253
action="store_true",

tests/unit/command/test_dag.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
import pytest
33

44
from dvc.cli import main, parse_args
5-
from dvc.commands.dag import CmdDAG, _build, _show_ascii, _show_dot, _show_mermaid
5+
from dvc.commands.dag import (
6+
CmdDAG,
7+
_build,
8+
_collapse_foreach_matrix,
9+
_show_ascii,
10+
_show_dot,
11+
_show_mermaid,
12+
)
13+
from dvc.parsing import JOIN
614

715

816
@pytest.mark.parametrize(
@@ -49,10 +57,53 @@ def repo(tmp_dir, dvc):
4957
return dvc
5058

5159

60+
def test_collapse_foreach_matrix(repo):
61+
graph = nx.DiGraph(
62+
[
63+
("2", "1"),
64+
("3@a", "2"),
65+
("3@a", "1"),
66+
("4", "1"),
67+
("3@b", "4"),
68+
("3@b", "1"),
69+
("5", "3@a"),
70+
("6", "3@b"),
71+
("7", "5"),
72+
("7", "6"),
73+
]
74+
)
75+
expected_graph = nx.DiGraph(
76+
[
77+
("2", "1"),
78+
("3", "2"),
79+
("4", "1"),
80+
("3", "4"),
81+
("3", "1"),
82+
("5", "3"),
83+
("6", "3"),
84+
("7", "5"),
85+
("7", "6"),
86+
]
87+
)
88+
collapsed_graph = _collapse_foreach_matrix(graph)
89+
for node in collapsed_graph.nodes:
90+
assert JOIN not in node
91+
for n1, n2 in collapsed_graph.edges:
92+
assert JOIN not in n1
93+
assert JOIN not in n2
94+
assert nx.is_isomorphic(collapsed_graph, expected_graph)
95+
96+
5297
def test_build(repo):
5398
assert nx.is_isomorphic(_build(repo), repo.index.graph)
5499

55100

101+
def test_build_collapse(repo):
102+
assert nx.is_isomorphic(
103+
_build(repo, collapse_foreach_matrix=True), repo.index.graph
104+
)
105+
106+
56107
def test_build_target(repo):
57108
graph = _build(repo, target="3")
58109
assert set(graph.nodes()) == {"3", "b.dvc", "a.dvc"}

0 commit comments

Comments
 (0)