Skip to content

Commit b15ad7b

Browse files
authored
Merge pull request #763 from tiran/depnode
Improve DependencyNode, get recursive build/install dependencies.
2 parents d052402 + 8a076c6 commit b15ad7b

2 files changed

Lines changed: 167 additions & 0 deletions

File tree

src/fromager/dependency_graph.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,60 @@ def construct_root_node(cls) -> DependencyNode:
115115
Version("0"),
116116
)
117117

118+
def iter_build_requirements(self) -> typing.Iterable[DependencyNode]:
119+
"""Get all unique, recursive build requirements
120+
121+
Yield all direct and indirect requirements to build the dependency.
122+
Includes direct build dependencies and their recursive **install**
123+
requirements.
124+
125+
The result is equivalent to the set of ``[build-system].requires``
126+
plus all ``Requires-Dist`` of build system requirements -- all
127+
packages in the build environment.
128+
"""
129+
visited: set[str] = set()
130+
# The outer loop iterates over all children and picks
131+
# direct build requirements. For each build requirement, it traverses
132+
# all children and recursively get their install requirements
133+
# (depth first).
134+
for edge in self.children:
135+
if edge.key in visited:
136+
# optimization: don't traverse visited nodes
137+
continue
138+
if not edge.req_type.is_build_requirement:
139+
# not a build requirement
140+
continue
141+
visited.add(edge.key)
142+
# it's a new ``[build-system].requires``.
143+
yield edge.destination_node
144+
# recursively get install dependencies of this build dep (depth first).
145+
for install_edge in self._traverse_install_requirements(
146+
edge.destination_node.children, visited
147+
):
148+
yield install_edge.destination_node
149+
150+
def iter_install_requirements(self) -> typing.Iterable[DependencyNode]:
151+
"""Get all unique, recursive install requirements"""
152+
visited: set[str] = set()
153+
for edge in self._traverse_install_requirements(self.children, visited):
154+
yield edge.destination_node
155+
156+
def _traverse_install_requirements(
157+
self,
158+
start_edges: list[DependencyEdge],
159+
visited: set[str],
160+
) -> typing.Iterable[DependencyEdge]:
161+
for edge in start_edges:
162+
if edge.key in visited:
163+
continue
164+
if not edge.req_type.is_install_requirement:
165+
continue
166+
visited.add(edge.destination_node.key)
167+
yield edge
168+
yield from self._traverse_install_requirements(
169+
edge.destination_node.children, visited
170+
)
171+
118172

119173
@dataclasses.dataclass(frozen=True, order=True, slots=True)
120174
class DependencyEdge:

tests/test_dependency_graph.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import dataclasses
2+
import graphlib
23

34
import pytest
5+
from packaging.requirements import Requirement
46
from packaging.utils import canonicalize_name
57
from packaging.version import Version
68

79
from fromager.dependency_graph import DependencyNode
10+
from fromager.requirements_file import RequirementType
811

912

1013
def mknode(name: str, version: str = "1.0", **kwargs) -> DependencyNode:
@@ -59,3 +62,113 @@ def test_dependencynode_dataclass():
5962
assert root.canonicalized_name == ""
6063
assert root.version == Version("0.0")
6164
assert root.key == ""
65+
66+
67+
def test_iter_requirements() -> None:
68+
a = mknode("a")
69+
# install requirements of a
70+
b = mknode("b")
71+
# build requirement of a
72+
c = mknode("c")
73+
# build requirement of c
74+
d = mknode("d")
75+
# install requirement of b and c
76+
e = mknode("e")
77+
# build requirement of a and c
78+
f = mknode("f")
79+
80+
a.add_child(b, Requirement(b.canonicalized_name), RequirementType.INSTALL)
81+
a.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
82+
a.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_SYSTEM)
83+
a.add_child(f, Requirement(c.canonicalized_name), RequirementType.BUILD_SYSTEM)
84+
b.add_child(e, Requirement(b.canonicalized_name), RequirementType.INSTALL)
85+
c.add_child(d, Requirement(d.canonicalized_name), RequirementType.BUILD_SYSTEM)
86+
c.add_child(e, Requirement(e.canonicalized_name), RequirementType.INSTALL)
87+
c.add_child(f, Requirement(f.canonicalized_name), RequirementType.BUILD_BACKEND)
88+
89+
assert sorted(a.iter_install_requirements()) == [b, e]
90+
assert sorted(a.iter_build_requirements()) == [c, e, f]
91+
assert sorted(b.iter_install_requirements()) == [e]
92+
assert sorted(b.iter_build_requirements()) == []
93+
assert sorted(c.iter_install_requirements()) == [e]
94+
assert sorted(c.iter_build_requirements()) == [d, f]
95+
96+
build_graph = get_build_graph(a, b, c, d, e, f)
97+
assert build_graph == [
98+
# no build requirements, B and E can be built in parallel, as
99+
# B just has an install requirement on E.
100+
["b", "d", "e", "f"],
101+
# C needs D, F to build.
102+
["c"],
103+
# A needs C, E, F.
104+
["a"],
105+
]
106+
107+
108+
def get_build_graph(*nodes: DependencyNode) -> list[list[str]]:
109+
topo: graphlib.TopologicalSorter[str] = graphlib.TopologicalSorter()
110+
for node in nodes:
111+
build_deps = [n.canonicalized_name for n in node.iter_build_requirements()]
112+
topo.add(node.canonicalized_name, *build_deps)
113+
topo.prepare()
114+
steps: list[list[str]] = []
115+
while topo.is_active():
116+
ready = topo.get_ready()
117+
steps.append(sorted(ready))
118+
topo.done(*ready)
119+
return steps
120+
121+
122+
def test_pr759_discussion() -> None:
123+
a = mknode("a")
124+
b = mknode("b")
125+
c = mknode("c")
126+
d = mknode("d")
127+
# A needs B to build.
128+
a.add_child(b, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
129+
# B needs C to build.
130+
b.add_child(c, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
131+
# B needs D to install.
132+
b.add_child(d, Requirement(c.canonicalized_name), RequirementType.INSTALL)
133+
134+
assert sorted(a.iter_build_requirements()) == [b, d]
135+
assert sorted(b.iter_build_requirements()) == [c]
136+
assert sorted(c.iter_build_requirements()) == []
137+
assert sorted(d.iter_build_requirements()) == []
138+
139+
build_graph = get_build_graph(a, b, c, d)
140+
assert build_graph == [["c", "d"], ["b"], ["a"]]
141+
142+
# add more nodes
143+
e = mknode("e")
144+
f = mknode("f")
145+
# D needs E to install.
146+
d.add_child(e, Requirement(c.canonicalized_name), RequirementType.INSTALL)
147+
# E needs F to build.
148+
e.add_child(f, Requirement(c.canonicalized_name), RequirementType.BUILD_BACKEND)
149+
150+
# build requirements
151+
assert sorted(a.iter_build_requirements()) == [b, d, e]
152+
assert sorted(b.iter_build_requirements()) == [c]
153+
assert sorted(c.iter_build_requirements()) == []
154+
assert sorted(d.iter_build_requirements()) == []
155+
assert sorted(e.iter_build_requirements()) == [f]
156+
157+
build_graph = get_build_graph(a, b, c, d, e, f)
158+
assert build_graph == [
159+
# D, C, F don't have build requirements
160+
["c", "d", "f"],
161+
# B needs C, E needs F
162+
["b", "e"],
163+
# A needs B, D, E
164+
["a"],
165+
]
166+
167+
# install requirements
168+
assert sorted(a.iter_install_requirements()) == []
169+
# E is an indirect install dependency
170+
assert sorted(b.iter_install_requirements()) == [d, e]
171+
assert sorted(c.iter_install_requirements()) == []
172+
assert sorted(d.iter_install_requirements()) == [e]
173+
assert sorted(e.iter_install_requirements()) == []
174+
assert sorted(f.iter_install_requirements()) == []

0 commit comments

Comments
 (0)