-
-
Notifications
You must be signed in to change notification settings - Fork 638
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added the
paths
goal to render all paths between two targets (#13778)
Fixes #13774 Usage: ``` $ ./pants paths --from=src/python/pants/backend/project_info/paths.py --to=src/python/pants/engine/addresses.py [ [ "src/python/pants/backend/project_info/paths.py", "src/python/pants/engine/addresses.py" ], [ "src/python/pants/backend/project_info/paths.py", "src/python/pants/engine/target.py", "src/python/pants/engine/addresses.py" ] ] ```
- Loading branch information
Showing
3 changed files
with
226 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
|
||
from __future__ import annotations | ||
|
||
import json | ||
from collections import deque | ||
from typing import Iterable, cast | ||
|
||
from pants.engine.addresses import Address, Addresses, UnparsedAddressInputs | ||
from pants.engine.console import Console | ||
from pants.engine.goal import Goal, GoalSubsystem, Outputting | ||
from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule | ||
from pants.engine.target import Dependencies as DependenciesField | ||
from pants.engine.target import ( | ||
DependenciesRequest, | ||
Targets, | ||
TransitiveTargets, | ||
TransitiveTargetsRequest, | ||
) | ||
|
||
|
||
class PathsSubsystem(Outputting, GoalSubsystem): | ||
name = "paths" | ||
help = "List the paths between two addresses." | ||
|
||
@classmethod | ||
def register_options(cls, register): | ||
super().register_options(register) | ||
register( | ||
"--from", | ||
type=str, | ||
dest="frm", | ||
help="The path starting address", | ||
) | ||
|
||
register( | ||
"--to", | ||
type=str, | ||
help="The path end address", | ||
) | ||
|
||
@property | ||
def path_from(self) -> str: | ||
return cast(str, self.options.frm) | ||
|
||
@property | ||
def path_to(self) -> str: | ||
return cast(str, self.options.to) | ||
|
||
|
||
class PathsGoal(Goal): | ||
subsystem_cls = PathsSubsystem | ||
|
||
|
||
def find_paths_breadth_first( | ||
adjacency_lists: dict[Address, Targets], from_target: Address, to_target: Address | ||
) -> Iterable[list[Address]]: | ||
"""Yields the paths between from_target to to_target if they exist. | ||
The paths are returned ordered by length, shortest first. If there are cycles, it checks visited | ||
edges to prevent recrossing them. | ||
""" | ||
|
||
if from_target == to_target: | ||
yield [from_target] | ||
return | ||
|
||
visited_edges = set() | ||
to_walk_paths = deque([[from_target]]) | ||
|
||
while len(to_walk_paths) > 0: | ||
cur_path = to_walk_paths.popleft() | ||
target = cur_path[-1] | ||
|
||
if len(cur_path) > 1: | ||
prev_target: Address | None = cur_path[-2] | ||
else: | ||
prev_target = None | ||
current_edge = (prev_target, target) | ||
|
||
if current_edge not in visited_edges: | ||
for dep in adjacency_lists[target]: | ||
dep_path = cur_path + [dep.address] | ||
if dep.address == to_target: | ||
yield dep_path | ||
else: | ||
to_walk_paths.append(dep_path) | ||
visited_edges.add(current_edge) | ||
|
||
|
||
@goal_rule | ||
async def paths( | ||
console: Console, addresses: Addresses, paths_subsystem: PathsSubsystem | ||
) -> PathsGoal: | ||
|
||
path_from = paths_subsystem.path_from | ||
path_to = paths_subsystem.path_to | ||
|
||
if path_from is None: | ||
raise ValueError("Must set a --paths-from") | ||
|
||
if path_to is None: | ||
raise ValueError("Must set a --paths-to") | ||
|
||
root, destination = await Get( | ||
Addresses, | ||
UnparsedAddressInputs(values=[path_from, path_to], owning_address=None), | ||
) | ||
|
||
transitive_targets = await Get( | ||
TransitiveTargets, TransitiveTargetsRequest([root], include_special_cased_deps=True) | ||
) | ||
|
||
if not any(destination == dep.address for dep in transitive_targets.closure): | ||
raise ValueError("The destination is not a dependency of the source") | ||
|
||
adjacent_targets_per_target = await MultiGet( | ||
Get( | ||
Targets, | ||
DependenciesRequest(tgt.get(DependenciesField), include_special_cased_deps=True), | ||
) | ||
for tgt in transitive_targets.closure | ||
) | ||
|
||
transitive_targets_closure_addresses = (t.address for t in transitive_targets.closure) | ||
adjacency_lists = dict(zip(transitive_targets_closure_addresses, adjacent_targets_per_target)) | ||
|
||
spec_paths = [] | ||
for path in find_paths_breadth_first(adjacency_lists, root, destination): | ||
spec_path = [address.spec for address in path] | ||
spec_paths.append(spec_path) | ||
|
||
with paths_subsystem.output(console) as write_stdout: | ||
write_stdout(json.dumps(spec_paths, indent=2)) | ||
|
||
return PathsGoal(exit_code=0) | ||
|
||
|
||
def rules(): | ||
return collect_rules() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). | ||
# Licensed under the Apache License, Version 2.0 (see LICENSE). | ||
import json | ||
from typing import List | ||
|
||
import pytest | ||
|
||
from pants.backend.project_info.paths import PathsGoal | ||
from pants.backend.project_info.paths import rules as paths_rules | ||
from pants.engine.internals.scheduler import ExecutionError | ||
from pants.engine.target import Dependencies, Target | ||
from pants.testutil.rule_runner import RuleRunner | ||
|
||
|
||
class MockTarget(Target): | ||
alias = "tgt" | ||
core_fields = (Dependencies,) | ||
|
||
|
||
@pytest.fixture | ||
def rule_runner() -> RuleRunner: | ||
runner = RuleRunner(rules=paths_rules(), target_types=[MockTarget]) | ||
runner.add_to_build_file("base", "tgt()") | ||
runner.add_to_build_file("intermediate", "tgt(dependencies=['base'])") | ||
runner.add_to_build_file("intermediate2", "tgt(dependencies=['base'])") | ||
runner.add_to_build_file("leaf", "tgt(dependencies=['intermediate', 'intermediate2'])") | ||
return runner | ||
|
||
|
||
def assert_paths( | ||
rule_runner: RuleRunner, | ||
*, | ||
path_from: str, | ||
path_to: str, | ||
expected: List[List[str]], | ||
) -> None: | ||
args = [] | ||
if path_from: | ||
args += [f"--paths-from={path_from}"] | ||
if path_to: | ||
args += [f"--paths-to={path_to}"] | ||
|
||
result = rule_runner.run_goal_rule(PathsGoal, args=[*args]) | ||
|
||
assert sorted(json.loads(result.stdout)) == sorted(expected) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"path_from,path_to", [["", ""], ["intermediate:intermediate", ""], ["", "base:base"]] | ||
) | ||
def test_no_targets(rule_runner: RuleRunner, path_from: str, path_to: str) -> None: | ||
with pytest.raises(ExecutionError): | ||
assert_paths(rule_runner, path_from=path_from, path_to=path_to, expected=[]) | ||
|
||
|
||
def test_normal(rule_runner: RuleRunner) -> None: | ||
assert_paths( | ||
rule_runner, | ||
path_from="intermediate:intermediate", | ||
path_to="base:base", | ||
expected=[["intermediate:intermediate", "base:base"]], | ||
) | ||
|
||
|
||
def test_path_to_self(rule_runner: RuleRunner) -> None: | ||
assert_paths( | ||
rule_runner, | ||
path_from="base:base", | ||
path_to="base:base", | ||
expected=[["base:base"]], | ||
) | ||
|
||
|
||
def test_multiple_paths(rule_runner: RuleRunner) -> None: | ||
assert_paths( | ||
rule_runner, | ||
path_from="leaf:leaf", | ||
path_to="base:base", | ||
expected=[ | ||
["leaf:leaf", "intermediate:intermediate", "base:base"], | ||
["leaf:leaf", "intermediate2:intermediate2", "base:base"], | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters