Skip to content

Commit 67a0a4e

Browse files
authored
Merge pull request #525 from rd4398/issue-524
Bug Fix: Duplicates are written to constraints file during bootstrapping
2 parents 2f76a26 + f6c46b5 commit 67a0a4e

File tree

2 files changed

+171
-51
lines changed

2 files changed

+171
-51
lines changed

src/fromager/commands/bootstrap.py

Lines changed: 98 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import click
55
from packaging.requirements import Requirement
6+
from packaging.utils import NormalizedName
7+
from packaging.version import Version
68

79
from .. import (
810
bootstrapper,
@@ -180,7 +182,17 @@ def write_constraints_file(
180182
conflicts = graph.get_install_dependency_versions()
181183
ret = True
182184
conflicting_deps = set()
183-
for dep_name, nodes in sorted(conflicts.items()):
185+
186+
# Map for already resolved versions for a given dependency Eg: {"a": "0.4"}
187+
resolved: dict[NormalizedName, Version] = {}
188+
189+
# List of unresolved dependencies
190+
unresolved_dependencies = sorted(conflicts.items())
191+
192+
dep_name: NormalizedName
193+
194+
# Loop over dependencies and resolve dependencies with single version first. This will shrink the unresolved_dependencies to begin with.
195+
for dep_name, nodes in unresolved_dependencies[:]:
184196
versions = [node.version for node in nodes]
185197
if len(versions) == 0:
186198
# This should never happen.
@@ -189,64 +201,99 @@ def write_constraints_file(
189201
if len(versions) == 1:
190202
# This is going to be the situation for most dependencies, where we
191203
# only have one version.
192-
output.write(f"{dep_name}=={versions[0]}\n")
193-
continue
194-
195-
# Below this point we have built multiple versions of the same thing, so
196-
# we need to try to determine if any one of those versions meets all of
197-
# the requirements.
198-
logger.debug("%s: found multiple versions in install requirements", dep_name)
199-
200-
# Track which versions can be used by which parent requirement.
201-
usable_versions: dict[str, list[str]] = {}
202-
# Track how many total users of a requirement (by name) there are so we
203-
# can tell later if any version can be used by all of them.
204-
user_counter = 0
205-
206-
# Which parent requirements can use which versions of the dependency we
207-
# are working on?
208-
for node in nodes:
209-
parent_edges = node.get_incoming_install_edges()
210-
user_counter += len(parent_edges)
211-
for parent_edge in parent_edges:
212-
for matching_version in parent_edge.req.specifier.filter(versions):
213-
usable_versions.setdefault(str(matching_version), []).append(
214-
str(parent_edge.destination_node.version)
204+
resolved[dep_name] = versions[0]
205+
# Remove from unresolved dependencies list
206+
unresolved_dependencies.remove((dep_name, nodes))
207+
multiple_versions = dict(unresolved_dependencies)
208+
209+
# Below this point we have built multiple versions of the same thing, so
210+
# we need to try to determine if any one of those versions meets all of
211+
# the requirements.
212+
213+
# Flag to see if something is resolved
214+
resolved_something = True
215+
216+
# Outer while loop to resolve remaining dependencies with multiple versions
217+
while unresolved_dependencies and resolved_something:
218+
resolved_something = False
219+
# Make copy of the original list and loop over unresolved dependencies
220+
for dep_name, nodes in unresolved_dependencies[:]:
221+
# Track which versions can be used by which parent requirement.
222+
usable_versions: dict[str, list[Version]] = {}
223+
# Track how many total users of a requirement (by name) there are so we
224+
# can tell later if any version can be used by all of them.
225+
user_counter = 0
226+
# Which parent requirements can use which versions of the dependency we
227+
# are working on?
228+
dep_versions = [node.version for node in nodes]
229+
# Loop over the nodes list
230+
for node in nodes:
231+
parent_edges = node.get_incoming_install_edges()
232+
# Loop over parent_edges list
233+
for parent_edge in parent_edges:
234+
parent_name = parent_edge.destination_node.canonicalized_name
235+
# Condition to select the right version.
236+
# We check whether parent_name is already in resolved dict and the version associated with that
237+
# is not the version of the destination node
238+
if (
239+
parent_name in resolved
240+
and resolved[parent_name]
241+
!= parent_edge.destination_node.version
242+
):
243+
continue
244+
# Loop to find the usable versions
245+
for matching_version in parent_edge.req.specifier.filter(
246+
dep_versions
247+
):
248+
usable_versions.setdefault(str(matching_version), []).append(
249+
parent_edge.destination_node.version
250+
)
251+
user_counter += 1
252+
253+
# Look for one version that can be used by all the parent dependencies
254+
# and output that if we find it. Otherwise, include a warning and report
255+
# all versions so a human reading the file can make their own decision
256+
# about how to resolve the conflict.
257+
for v, users in usable_versions.items():
258+
if len(users) == user_counter:
259+
version_strs = [str(v) for v in sorted(versions)]
260+
logger.debug(
261+
"%s: selecting %s from multiple candidates %s",
262+
dep_name,
263+
v,
264+
version_strs,
215265
)
266+
resolved[dep_name] = Version(v)
267+
resolved_something = True
268+
unresolved_dependencies.remove((dep_name, nodes))
216269

217-
# Look for one version that can be used by all the parent dependencies
218-
# and output that if we find it. Otherwise, include a warning and report
219-
# all versions so a human reading the file can make their own decision
220-
# about how to resolve the conflict.
221-
for v, users in usable_versions.items():
222-
if len(users) == user_counter:
223-
version_strs = [str(v) for v in sorted(versions)]
224-
output.write(
225-
f"# NOTE: fromager selected {dep_name}=={v} from: {version_strs}\n"
226-
)
227-
logger.debug(
228-
"%s: selecting %s from multiple candidates %s",
229-
dep_name,
230-
v,
231-
version_strs,
232-
)
233-
output.write(f"{dep_name}=={v}\n")
234-
break
235-
else:
236-
# No single version could be used, so go ahead and print all the
237-
# versions with a warning message
238-
ret = False
270+
# Write resolved versions to constraints file
271+
for dep_name, resolved_version in sorted(resolved.items()):
272+
if dep_name in multiple_versions:
273+
version_strs = [
274+
str(node.version)
275+
for node in sorted(multiple_versions[dep_name], key=lambda n: n.version)
276+
]
239277
output.write(
240-
f"# ERROR: no single version of {dep_name} met all requirements\n"
278+
f"# NOTE: fromager selected {dep_name}=={resolved_version} from: {version_strs}\n"
241279
)
242-
logger.error("%s: no single version meets all requirements", dep_name)
243-
conflicting_deps.add(dep_name)
244-
for dv in sorted(versions):
245-
output.write(f"{dep_name}=={dv}\n")
280+
output.write(f"{dep_name}=={resolved_version}\n")
281+
282+
# No single version could be used, so go ahead and print all the
283+
# versions with a warning message
284+
for dep_name, nodes in unresolved_dependencies:
285+
ret = False
286+
logger.error("%s: no single version meets all requirements", dep_name)
287+
output.write(f"# ERROR: no single version of {dep_name} met all requirements\n")
288+
conflicting_deps.add(dep_name)
289+
for node in sorted(nodes, key=lambda n: n.version):
290+
output.write(f"{dep_name}=={node.version}\n")
291+
246292
for dep_name in conflicting_deps:
247293
logger.error("finding why %s was being used", dep_name)
248294
for node in graph.get_nodes_by_name(dep_name):
249295
find_why(graph, node, -1, 1, [])
296+
250297
return ret
251298

252299

tests/test_bootstrap.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,76 @@ def test_write_constraints_file_unresolvable_duplicate():
188188
c==3.1
189189
""").lstrip()
190190
assert expected == buffer.getvalue()
191+
192+
193+
def test_write_constraints_file_duplicates():
194+
buffer = io.StringIO()
195+
raw_graph = {
196+
"": {
197+
"download_url": "",
198+
"pre_built": False,
199+
"version": "0",
200+
"canonicalized_name": "",
201+
"edges": [
202+
{"key": "a==1.0", "req_type": "install", "req": "a"},
203+
{"key": "d==1.0", "req_type": "install", "req": "d"},
204+
],
205+
},
206+
"a==1.0": {
207+
"download_url": "url for a",
208+
"pre_built": False,
209+
"version": "1.0",
210+
"canonicalized_name": "a",
211+
"edges": [
212+
{"key": "c==3.0", "req_type": "install", "req": "c<=3.0"},
213+
],
214+
},
215+
"d==1.0": {
216+
"download_url": "url for a",
217+
"pre_built": False,
218+
"version": "1.0",
219+
"canonicalized_name": "a",
220+
"edges": [
221+
{"key": "c==3.1", "req_type": "install", "req": "c>=3.0"},
222+
],
223+
},
224+
"c==3.0": { # transformers 4.46
225+
"download_url": "url for c",
226+
"pre_built": False,
227+
"version": "3.0",
228+
"canonicalized_name": "c",
229+
"edges": [{"key": "b==2.0", "req_type": "install", "req": "b<2.1,>=2.0"}],
230+
},
231+
"c==3.1": { # transformers 4.47
232+
"download_url": "url for c",
233+
"pre_built": False,
234+
"version": "3.1",
235+
"canonicalized_name": "c",
236+
"edges": [{"key": "b==2.1", "req_type": "install", "req": "b<2.2,>=2.1"}],
237+
},
238+
"b==2.0": { # tokenizer
239+
"download_url": "url for b",
240+
"pre_built": False,
241+
"version": "2.0",
242+
"canonicalized_name": "b",
243+
"edges": [],
244+
},
245+
"b==2.1": { # tokenizer
246+
"download_url": "url for b",
247+
"pre_built": False,
248+
"version": "2.1",
249+
"canonicalized_name": "b",
250+
"edges": [],
251+
},
252+
}
253+
graph = dependency_graph.DependencyGraph.from_dict(raw_graph)
254+
assert bootstrap.write_constraints_file(graph, buffer)
255+
expected = textwrap.dedent("""
256+
a==1.0
257+
# NOTE: fromager selected b==2.0 from: ['2.0', '2.1']
258+
b==2.0
259+
# NOTE: fromager selected c==3.0 from: ['3.0', '3.1']
260+
c==3.0
261+
d==1.0
262+
""").lstrip()
263+
assert expected == buffer.getvalue()

0 commit comments

Comments
 (0)