Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/fromager/bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,9 @@ def _add_to_graph(

_, parent_req, parent_version = self.why[-1] if self.why else (None, None, None)
pbi = self.ctx.package_build_info(req)
# Get the constraint rule if any
constraint_req = self.ctx.constraints.get_constraint(req.name)
constraint = str(constraint_req) if constraint_req else ""
# Update the dependency graph after we determine that this requirement is
# useful but before we determine if it is redundant so that we capture all
# edges to use for building a valid constraints file.
Expand All @@ -950,6 +953,7 @@ def _add_to_graph(
req_version=req_version,
download_url=download_url,
pre_built=pbi.pre_built,
constraint=constraint,
)
self.ctx.write_to_graph_to_file()

Expand Down
4 changes: 4 additions & 0 deletions src/fromager/commands/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def bootstrap(
req_type=RequirementType.TOP_LEVEL,
)
logger.info("%s resolves to %s", req, version)
# Get the constraint rule if any
constraint_req = wkctx.constraints.get_constraint(req.name)
constraint = str(constraint_req) if constraint_req else ""
wkctx.dependency_graph.add_dependency(
parent_name=None,
parent_version=None,
Expand All @@ -177,6 +180,7 @@ def bootstrap(
req_version=version,
download_url=source_url,
pre_built=pbi.pre_built,
constraint=constraint,
)
requirement_ctxvar.reset(token)

Expand Down
22 changes: 18 additions & 4 deletions src/fromager/commands/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,11 @@ def show_explain_duplicates(graph: DependencyGraph) -> None:
usable_versions: dict[str, list[str]] = {}
user_counter: int = 0

print(f"\n{dep_name}")
# Get the constraint from the first node (all versions have the same constraint)
constraint_info = (
f" (constraint: {nodes[0].constraint})" if nodes[0].constraint else ""
)
print(f"\n{dep_name}{constraint_info}")
for node in sorted(nodes, key=lambda x: x.version):
print(f" {node.version}")

Expand Down Expand Up @@ -327,7 +331,8 @@ def find_why(
# we might be invoked for multiple packages and we want the format to be
# consistent.
if depth == 0:
print(f"\n{node.key}")
constraint_info = f" (constraint: {node.constraint})" if node.constraint else ""
print(f"\n{node.key}{constraint_info}")

seen = set([node.key]).union(seen)
all_skipped = True
Expand All @@ -338,16 +343,25 @@ def find_why(
# dependencies.
if parent.destination_node.key == ROOT:
is_toplevel = True
# Show constraint for top-level dependencies
constraint_info = (
f" (constraint: {node.constraint})" if node.constraint else ""
)
print(
f"{' ' * depth} * {node.key} is a toplevel dependency with req {parent.req}"
f"{' ' * depth} * {node.key}{constraint_info} is a toplevel dependency with req {parent.req}"
)
continue
# Skip dependencies that don't match the req_type.
if req_type and parent.req_type not in req_type:
continue
all_skipped = False
parent_constraint = (
f" (constraint: {parent.destination_node.constraint})"
if parent.destination_node.constraint
else ""
)
print(
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key}{parent_constraint} with req {parent.req}"
)
if max_depth and (max_depth == -1 or depth <= max_depth):
find_why(
Expand Down
8 changes: 8 additions & 0 deletions src/fromager/dependency_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class DependencyNodeDict(typing.TypedDict):
canonicalized_name: str
version: str
pre_built: bool
constraint: str
edges: list[DependencyEdgeDict]


Expand All @@ -38,6 +39,7 @@ class DependencyNode:
version: Version
download_url: str = dataclasses.field(default="", compare=False)
pre_built: bool = dataclasses.field(default=False, compare=False)
constraint: str = dataclasses.field(default="", compare=False)
# additional fields
key: str = dataclasses.field(init=False, compare=False, repr=False)
parents: list[DependencyEdge] = dataclasses.field(
Expand Down Expand Up @@ -85,6 +87,7 @@ def to_dict(self) -> DependencyNodeDict:
"pre_built": self.pre_built,
"version": str(self.version),
"canonicalized_name": str(self.canonicalized_name),
"constraint": self.constraint,
"edges": [edge.to_dict() for edge in self.children],
}

Expand Down Expand Up @@ -175,6 +178,7 @@ def from_dict(
req_version=Version(destination_node_dict["version"]),
download_url=destination_node_dict["download_url"],
pre_built=destination_node_dict["pre_built"],
constraint=destination_node_dict.get("constraint", ""),
)
stack.append(edge_dict["key"])
visited.add(curr_key)
Expand Down Expand Up @@ -207,12 +211,14 @@ def _add_node(
version: Version,
download_url: str,
pre_built: bool,
constraint: str,
):
new_node = DependencyNode(
canonicalized_name=req_name,
version=version,
download_url=download_url,
pre_built=pre_built,
constraint=constraint,
)
# check if a node with that key already exists. if it does then use that
node = self.nodes.get(new_node.key, new_node)
Expand All @@ -229,6 +235,7 @@ def add_dependency(
req_version: Version,
download_url: str = "",
pre_built: bool = False,
constraint: str = "",
) -> None:
logger.debug(
"recording %s dependency %s%s -> %s==%s",
Expand All @@ -244,6 +251,7 @@ def add_dependency(
version=req_version,
download_url=download_url,
pre_built=pre_built,
constraint=constraint,
)

parent_key = ROOT if parent_name is None else f"{parent_name}=={parent_version}"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dependency_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_dependencynode_dataclass():
assert a.key == "a==1.0"
assert (
repr(a)
== "DependencyNode(canonicalized_name='a', version=<Version('1.0')>, download_url='', pre_built=False)"
== "DependencyNode(canonicalized_name='a', version=<Version('1.0')>, download_url='', pre_built=False, constraint='')"
)
with pytest.raises(dataclasses.FrozenInstanceError):
a.version = Version("2.0")
Expand Down
4 changes: 4 additions & 0 deletions tests/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"pre_built": False,
"version": "0",
"canonicalized_name": "",
"constraint": "",
"edges": [{"key": "a==2.0", "req_type": "install", "req": "a==2.0"}],
},
"a==2.0": {
"download_url": "url",
"pre_built": False,
"version": "2.0",
"canonicalized_name": "a",
"constraint": "",
"edges": [
{"key": "b==3.0", "req_type": "build-system", "req": "b==3.0"},
{"key": "c==4.0", "req_type": "build-backend", "req": "c==4.0"},
Expand All @@ -28,6 +30,7 @@
"pre_built": False,
"version": "3.0",
"canonicalized_name": "b",
"constraint": "",
"edges": [
{"key": "c==4.0", "req_type": "build-sdist", "req": "c<=4.0"},
],
Expand All @@ -37,6 +40,7 @@
"pre_built": False,
"version": "4.0",
"canonicalized_name": "c",
"constraint": "",
"edges": [],
},
}
Expand Down
182 changes: 182 additions & 0 deletions tests/test_graph_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"""Test graph command functions that display constraint information."""

from packaging.requirements import Requirement
from packaging.utils import canonicalize_name
from packaging.version import Version

from fromager import dependency_graph
from fromager.commands.graph import find_why, show_explain_duplicates
from fromager.requirements_file import RequirementType


def test_show_explain_duplicates_with_constraints(capsys):
"""Test that explain_duplicates shows constraint information."""
# Create a graph with duplicate dependencies that have constraints
graph = dependency_graph.DependencyGraph()

# Add top-level package
graph.add_dependency(
parent_name=None,
parent_version=None,
req_type=RequirementType.TOP_LEVEL,
req=Requirement("package-a"),
req_version=Version("1.0.0"),
download_url="https://example.com/package-a-1.0.0.tar.gz",
)

# Add package-b version 1.0.0 as dependency of package-a with constraint
graph.add_dependency(
parent_name=canonicalize_name("package-a"),
parent_version=Version("1.0.0"),
req_type=RequirementType.INSTALL,
req=Requirement("package-b>=1.0"),
req_version=Version("1.0.0"),
download_url="https://example.com/package-b-1.0.0.tar.gz",
constraint="package-b>=1.0,<2.0",
)

# Add another top-level package
graph.add_dependency(
parent_name=None,
parent_version=None,
req_type=RequirementType.TOP_LEVEL,
req=Requirement("package-c"),
req_version=Version("1.0.0"),
download_url="https://example.com/package-c-1.0.0.tar.gz",
)

# Add package-b version 2.0.0 as dependency of package-c without constraint
graph.add_dependency(
parent_name=canonicalize_name("package-c"),
parent_version=Version("1.0.0"),
req_type=RequirementType.INSTALL,
req=Requirement("package-b>=2.0"),
req_version=Version("2.0.0"),
download_url="https://example.com/package-b-2.0.0.tar.gz",
constraint="",
)

# Run the command
show_explain_duplicates(graph)

# Capture output
captured = capsys.readouterr()

# Verify constraint is shown at the package name level, not per-version
assert "package-b (constraint: package-b>=1.0,<2.0)" in captured.out
# Versions should be shown without constraint info
assert " 1.0.0\n" in captured.out
assert " 2.0.0\n" in captured.out
# Version lines should not have constraint info
assert "1.0.0 (constraint:" not in captured.out
assert "2.0.0 (constraint:" not in captured.out


def test_find_why_with_constraints(capsys):
"""Test that why command shows constraint information."""
# Create a graph with constraints
graph = dependency_graph.DependencyGraph()

# Add top-level package with constraint
graph.add_dependency(
parent_name=None,
parent_version=None,
req_type=RequirementType.TOP_LEVEL,
req=Requirement("parent-pkg"),
req_version=Version("1.0.0"),
download_url="https://example.com/parent-pkg-1.0.0.tar.gz",
constraint="parent-pkg==1.0.0",
)

# Add child dependency with its own constraint
graph.add_dependency(
parent_name=canonicalize_name("parent-pkg"),
parent_version=Version("1.0.0"),
req_type=RequirementType.INSTALL,
req=Requirement("child-pkg>=1.0"),
req_version=Version("1.5.0"),
download_url="https://example.com/child-pkg-1.5.0.tar.gz",
constraint="child-pkg>=1.0,<2.0",
)

# Find why child-pkg is included
child_node = graph.nodes["child-pkg==1.5.0"]
find_why(graph, child_node, 1, 0, [])

# Capture output
captured = capsys.readouterr()

# Verify constraint is shown for the child package at depth 0
assert "child-pkg==1.5.0 (constraint: child-pkg>=1.0,<2.0)" in captured.out
# Verify constraint is shown for the parent when showing the dependency relationship
assert "(constraint: parent-pkg==1.0.0)" in captured.out


def test_find_why_toplevel_with_constraint(capsys):
"""Test that why command shows constraint for top-level dependencies."""
# Create a graph with a top-level package that has a constraint
graph = dependency_graph.DependencyGraph()

# Add top-level package with constraint
graph.add_dependency(
parent_name=None,
parent_version=None,
req_type=RequirementType.TOP_LEVEL,
req=Requirement("toplevel-pkg"),
req_version=Version("2.0.0"),
download_url="https://example.com/toplevel-pkg-2.0.0.tar.gz",
constraint="toplevel-pkg>=2.0,<3.0",
)

# Find why toplevel-pkg is included
node = graph.nodes["toplevel-pkg==2.0.0"]
find_why(graph, node, 0, 0, [])

# Capture output
captured = capsys.readouterr()

# Verify constraint is shown at depth 0
assert "toplevel-pkg==2.0.0 (constraint: toplevel-pkg>=2.0,<3.0)" in captured.out
# Verify constraint is shown when identifying it as a top-level dependency
assert (
"toplevel-pkg==2.0.0 (constraint: toplevel-pkg>=2.0,<3.0) is a toplevel dependency"
in captured.out
)


def test_find_why_without_constraints(capsys):
"""Test that why command works when no constraints are present."""
# Create a graph without constraints
graph = dependency_graph.DependencyGraph()

# Add top-level package without constraint
graph.add_dependency(
parent_name=None,
parent_version=None,
req_type=RequirementType.TOP_LEVEL,
req=Requirement("simple-pkg"),
req_version=Version("1.0.0"),
download_url="https://example.com/simple-pkg-1.0.0.tar.gz",
)

# Add child dependency without constraint
graph.add_dependency(
parent_name=canonicalize_name("simple-pkg"),
parent_version=Version("1.0.0"),
req_type=RequirementType.INSTALL,
req=Requirement("simple-child"),
req_version=Version("2.0.0"),
download_url="https://example.com/simple-child-2.0.0.tar.gz",
)

# Find why simple-child is included
child_node = graph.nodes["simple-child==2.0.0"]
find_why(graph, child_node, 1, 0, [])

# Capture output
captured = capsys.readouterr()

# Verify no constraint info is shown
assert "(constraint:" not in captured.out
assert "simple-child==2.0.0" in captured.out
assert "simple-pkg==1.0.0" in captured.out
Loading