Skip to content

Commit 0f074e8

Browse files
Extend dependents goal with output format to support JSON
1 parent fb615a4 commit 0f074e8

File tree

2 files changed

+160
-13
lines changed

2 files changed

+160
-13
lines changed

src/python/pants/backend/project_info/dependents.py

+70-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3-
3+
import json
44
from collections import defaultdict
55
from dataclasses import dataclass
6+
from enum import Enum
67
from typing import Iterable, Set
78

89
from pants.engine.addresses import Address, Addresses
@@ -16,7 +17,7 @@
1617
Dependencies,
1718
DependenciesRequest,
1819
)
19-
from pants.option.option_types import BoolOption
20+
from pants.option.option_types import BoolOption, EnumOption
2021
from pants.util.frozendict import FrozenDict
2122
from pants.util.logging import LogLevel
2223
from pants.util.ordered_set import FrozenOrderedSet
@@ -27,6 +28,17 @@ class AddressToDependents:
2728
mapping: FrozenDict[Address, FrozenOrderedSet[Address]]
2829

2930

31+
class DependentsOutputFormat(Enum):
32+
"""Output format for listing dependents.
33+
34+
text: List all dependents as a single list of targets in plain text.
35+
json: List all dependents as a mapping `{target: [dependents]}`.
36+
"""
37+
38+
text = "text"
39+
json = "json"
40+
41+
3042
@rule(desc="Map all targets to their dependents", level=LogLevel.DEBUG)
3143
async def map_addresses_to_dependents(all_targets: AllUnexpandedTargets) -> AddressToDependents:
3244
dependencies_per_target = await MultiGet(
@@ -105,7 +117,12 @@ class DependentsSubsystem(LineOriented, GoalSubsystem):
105117
)
106118
closed = BoolOption(
107119
default=False,
108-
help="Include the input targets in the output, along with the dependents.",
120+
help="Include the input targets in the output, along with the dependents. This option "
121+
"only applies when using the `text` format.",
122+
)
123+
format = EnumOption(
124+
default=DependentsOutputFormat.text,
125+
help="Output format for listing dependents.",
109126
)
110127

111128

@@ -114,21 +131,66 @@ class DependentsGoal(Goal):
114131
environment_behavior = Goal.EnvironmentBehavior.LOCAL_ONLY
115132

116133

117-
@goal_rule
118-
async def dependents_goal(
119-
specified_addresses: Addresses, dependents_subsystem: DependentsSubsystem, console: Console
120-
) -> DependentsGoal:
134+
async def list_dependents_as_plain_text(
135+
addresses: Addresses, dependents_subsystem: DependentsSubsystem, console: Console
136+
) -> None:
137+
"""Get dependents for given addresses and list them in the console as a single list."""
121138
dependents = await Get(
122139
Dependents,
123140
DependentsRequest(
124-
specified_addresses,
141+
addresses,
125142
transitive=dependents_subsystem.transitive,
126143
include_roots=dependents_subsystem.closed,
127144
),
128145
)
129146
with dependents_subsystem.line_oriented(console) as print_stdout:
130147
for address in dependents:
131148
print_stdout(address.spec)
149+
150+
151+
async def list_dependents_as_json(
152+
addresses: Addresses, dependents_subsystem: DependentsSubsystem, console: Console
153+
) -> None:
154+
"""Get dependents for given addresses and list them in the console in JSON.
155+
156+
Note that `--closed` option is ignored as it doesn't make sense to duplicate source address in
157+
the list of its dependents.
158+
"""
159+
dependents_group = await MultiGet(
160+
Get(
161+
Dependents,
162+
DependentsRequest(
163+
(address,),
164+
transitive=dependents_subsystem.transitive,
165+
include_roots=False,
166+
),
167+
)
168+
for address in addresses
169+
)
170+
iterated_addresses = []
171+
for dependents in dependents_group:
172+
iterated_addresses.append(sorted([str(address) for address in dependents]))
173+
mapping = dict(zip([str(address) for address in addresses], iterated_addresses))
174+
output = json.dumps(mapping, indent=4)
175+
console.print_stdout(output)
176+
177+
178+
@goal_rule
179+
async def dependents_goal(
180+
specified_addresses: Addresses, dependents_subsystem: DependentsSubsystem, console: Console
181+
) -> DependentsGoal:
182+
if DependentsOutputFormat.text == dependents_subsystem.format:
183+
await list_dependents_as_plain_text(
184+
addresses=specified_addresses,
185+
dependents_subsystem=dependents_subsystem,
186+
console=console,
187+
)
188+
elif DependentsOutputFormat.json == dependents_subsystem.format:
189+
await list_dependents_as_json(
190+
addresses=specified_addresses,
191+
dependents_subsystem=dependents_subsystem,
192+
console=console,
193+
)
132194
return DependentsGoal(exit_code=0)
133195

134196

src/python/pants/backend/project_info/dependents_test.py

+90-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3-
4-
from typing import List
3+
import json
4+
from functools import partial
5+
from typing import Dict, List, Union
56

67
import pytest
78

8-
from pants.backend.project_info.dependents import DependentsGoal
9+
from pants.backend.project_info.dependents import DependentsGoal, DependentsOutputFormat
910
from pants.backend.project_info.dependents import rules as dependent_rules
1011
from pants.engine.target import Dependencies, SpecialCasedDependencies, Target
1112
from pants.testutil.rule_runner import RuleRunner
@@ -41,17 +42,22 @@ def assert_dependents(
4142
rule_runner: RuleRunner,
4243
*,
4344
targets: List[str],
44-
expected: List[str],
45+
expected: Union[List[str], Dict[str, List[str]]],
4546
transitive: bool = False,
4647
closed: bool = False,
48+
output_format: DependentsOutputFormat = DependentsOutputFormat.text,
4749
) -> None:
4850
args = []
4951
if transitive:
5052
args.append("--transitive")
5153
if closed:
5254
args.append("--closed")
55+
args.append(f"--format={output_format.value}")
5356
result = rule_runner.run_goal_rule(DependentsGoal, args=[*args, *targets])
54-
assert result.stdout.splitlines() == expected
57+
if output_format == DependentsOutputFormat.text:
58+
assert result.stdout.splitlines() == expected
59+
elif output_format == DependentsOutputFormat.json:
60+
assert json.loads(result.stdout) == expected
5561

5662

5763
def test_no_targets(rule_runner: RuleRunner) -> None:
@@ -102,3 +108,82 @@ def test_special_cased_dependencies(rule_runner: RuleRunner) -> None:
102108
transitive=True,
103109
expected=["intermediate:intermediate", "leaf:leaf", "special:special"],
104110
)
111+
112+
113+
def test_dependents_as_json_direct_deps(rule_runner: RuleRunner) -> None:
114+
rule_runner.write_files({"special/BUILD": "tgt(special_deps=['intermediate'])"})
115+
assert_deps = partial(
116+
assert_dependents,
117+
rule_runner,
118+
output_format=DependentsOutputFormat.json,
119+
)
120+
# input: single target
121+
assert_deps(
122+
targets=["base"],
123+
transitive=False,
124+
expected={
125+
"base:base": ["intermediate:intermediate"],
126+
},
127+
)
128+
129+
# input: multiple targets
130+
assert_deps(
131+
targets=["base", "intermediate"],
132+
transitive=False,
133+
expected={
134+
"base:base": ["intermediate:intermediate"],
135+
"intermediate:intermediate": ["leaf:leaf", "special:special"],
136+
},
137+
)
138+
139+
# input: all targets
140+
assert_deps(
141+
targets=["::"],
142+
transitive=False,
143+
expected={
144+
"base:base": ["intermediate:intermediate"],
145+
"intermediate:intermediate": ["leaf:leaf", "special:special"],
146+
"leaf:leaf": [],
147+
"special:special": [],
148+
},
149+
)
150+
151+
152+
def test_dependents_as_json_transitive_deps(rule_runner: RuleRunner) -> None:
153+
rule_runner.write_files({"special/BUILD": "tgt(special_deps=['intermediate'])"})
154+
assert_deps = partial(
155+
assert_dependents,
156+
rule_runner,
157+
output_format=DependentsOutputFormat.json,
158+
)
159+
160+
# input: single target
161+
assert_deps(
162+
targets=["base"],
163+
transitive=True,
164+
expected={
165+
"base:base": ["intermediate:intermediate", "leaf:leaf", "special:special"],
166+
},
167+
)
168+
169+
# input: multiple targets
170+
assert_deps(
171+
targets=["base", "intermediate"],
172+
transitive=True,
173+
expected={
174+
"base:base": ["intermediate:intermediate", "leaf:leaf", "special:special"],
175+
"intermediate:intermediate": ["leaf:leaf", "special:special"],
176+
},
177+
)
178+
179+
# input: all targets
180+
assert_deps(
181+
targets=["::"],
182+
transitive=False,
183+
expected={
184+
"base:base": ["intermediate:intermediate"],
185+
"intermediate:intermediate": ["leaf:leaf", "special:special"],
186+
"leaf:leaf": [],
187+
"special:special": [],
188+
},
189+
)

0 commit comments

Comments
 (0)