Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --output-format=json output option to v2 list #8450

Closed
Closed
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
1 change: 1 addition & 0 deletions src/python/pants/base/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ python_library(
name = 'hash_utils',
sources = ['hash_utils.py'],
dependencies = [
'3rdparty/python:dataclasses',
'3rdparty/python:typing-extensions',
'src/python/pants/util:strutil',
'src/python/pants/util:ordered_set',
Expand Down
4 changes: 4 additions & 0 deletions src/python/pants/base/hash_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import dataclasses
import hashlib
import json
import logging
Expand Down Expand Up @@ -140,6 +141,9 @@ def default(self, o):
)
# Set order is arbitrary in python 3.6 and 3.7, so we need to keep this sorted() call.
return sorted(self.default(i) for i in o)
if dataclasses.is_dataclass(o):
# `@dataclass` objects will fail with a cyclic reference error unless we stringify them here.
return self.default(repr(o))
if isinstance(o, Iterable) and not isinstance(o, (bytes, list, str)):
return list(self.default(i) for i in o)
logger.debug(
Expand Down
229 changes: 159 additions & 70 deletions src/python/pants/core/project_info/list_targets.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,202 @@
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from typing import Dict, cast
import json
from enum import Enum
from typing import Callable, Dict, Optional, Union, cast

from pants.engine.addresses import Address, Addresses
from pants.engine.console import Console
from pants.engine.goal import Goal, GoalSubsystem, LineOriented
from pants.engine.legacy.graph import FingerprintedTargetCollection, TransitiveFingerprintedTarget
from pants.engine.rules import goal_rule
from pants.engine.selectors import Get
from pants.engine.target import DescriptionField, ProvidesField, Targets
from pants.engine.target import DescriptionField, ProvidesField, Target, Targets


class ListOptions(LineOriented, GoalSubsystem):
"""Lists all targets matching the file or target arguments."""

name = "list-v2"

class OutputFormat(Enum):
address_specs = "address-specs"
provides = "provides"
documented = "documented"
json = "json"

@classmethod
def register_options(cls, register):
super().register_options(register)
register(
"--provides",
type=bool,
help=(
"List only targets that provide an artifact, displaying the columns specified by "
"--provides-columns."
),
removal_version="1.30.0.dev2",
removal_hint="Use --output-format=provides instead!",
help="List only targets that provide an artifact, displaying the columns specified by "
"--provides-columns.",
)
register(
"--provides-columns",
default="address,artifact_id",
help=(
"Display these columns when --provides is specified. Available columns are: "
"address, artifact_id, repo_name, repo_url, push_db_basedir"
),
help="Display these columns when --output-format=provides is specified. Available "
"columns are: address, artifact_id, repo_name, repo_url, push_db_basedir",
)

register(
"--documented",
type=bool,
removal_version="1.30.0.dev2",
removal_hint="Use --output-format=documented instead!",
help="Print only targets that are documented with a description.",
)


class List(Goal):
subsystem_cls = ListOptions
register(
"--output-format",
type=cls.OutputFormat,
default=cls.OutputFormat.address_specs,
help="How to format targets when printed to stdout.",
)


@goal_rule
async def list_targets(addresses: Addresses, options: ListOptions, console: Console) -> List:
if not addresses.dependencies:
console.print_stderr(f"WARNING: No targets were matched in goal `{options.name}`.")
return List(exit_code=0)

provides_enabled = options.values.provides
documented_enabled = options.values.documented
if provides_enabled and documented_enabled:
PrintFunction = Callable[[Target], Optional[str]]


def _make_provides_print_fn(provides_columns: str, targets: Targets) -> PrintFunction:
addresses_with_provide_artifacts = {
tgt.address: tgt[ProvidesField].value
for tgt in targets
if tgt.get(ProvidesField).value is not None
}
extractor_funcs = {
"address": lambda address, _: address.spec,
"artifact_id": lambda _, artifact: str(artifact),
"repo_name": lambda _, artifact: artifact.repo.name,
"repo_url": lambda _, artifact: artifact.repo.url,
"push_db_basedir": lambda _, artifact: artifact.repo.push_db_basedir,
}
try:
column_extractors = [
extractor_funcs[col] for col in provides_columns.split(",")
]
except KeyError:
raise ValueError(
"Cannot specify both `--list-documented` and `--list-provides` at the same time. "
"Please choose one."
"Invalid columns provided for `--list-provides-columns`: "
f"{provides_columns}. Valid columns are: "
f"{', '.join(sorted(extractor_funcs.keys()))}."
)

try:
column_extractors = [extractor_funcs[col] for col in (provides_columns.split(","))]
except KeyError:
raise Exception(
"Invalid columns specified: {0}. Valid columns are: address, artifact_id, "
"repo_name, repo_url, push_db_basedir.".format(provides_columns)
)

if provides_enabled:
targets = await Get[Targets](Addresses, addresses)
addresses_with_provide_artifacts = {
tgt.address: tgt[ProvidesField].value
def print_provides(target: Target) -> Optional[str]:
address = target.address
artifact = addresses_with_provide_artifacts.get(address, None)
if artifact:
return " ".join(extractor(address, artifact) for extractor in column_extractors)
return None

return print_provides


def _make_print_documented_target(targets: Targets) -> PrintFunction:
addresses_with_descriptions = cast(
Dict[Address, str],
{
tgt.address: tgt[DescriptionField].value
for tgt in targets
if tgt.get(ProvidesField).value is not None
if tgt.get(DescriptionField).value is not None
},
)
def print_documented(target: Target) -> Optional[str]:
address = target.address
description = addresses_with_descriptions.get(address, None)
if description:
formatted_description = "\n ".join(description.strip().split("\n"))
return f"{address.spec}\n {formatted_description}"
return None
return print_documented


FingerprintedPrintFunction = Callable[[TransitiveFingerprintedTarget], str]


def _print_fingerprinted_target(fingerprinted_target: TransitiveFingerprintedTarget) -> str:
was_root = fingerprinted_target.was_root
address = fingerprinted_target.address.spec
target_type = fingerprinted_target.type_alias
intransitive_fingerprint = fingerprinted_target.intransitive_fingerprint_arg
transitive_fingerprint = fingerprinted_target.transitive_fingerprint_arg
return json.dumps(
{
"was_root": was_root,
"address": address,
"target_type": target_type,
"intransitive_fingerprint": intransitive_fingerprint,
"transitive_fingerprint": transitive_fingerprint,
}
extractor_funcs = {
"address": lambda address, _: address.spec,
"artifact_id": lambda _, artifact: str(artifact),
"repo_name": lambda _, artifact: artifact.repo.name,
"repo_url": lambda _, artifact: artifact.repo.url,
"push_db_basedir": lambda _, artifact: artifact.repo.push_db_basedir,
}
try:
extractors = [
extractor_funcs[col] for col in options.values.provides_columns.split(",")
]
except KeyError:
raise ValueError(
"Invalid columns provided for `--list-provides-columns`: "
f"{options.values.provides_columns}. Valid columns are: "
f"{', '.join(sorted(extractor_funcs.keys()))}."
)
with options.line_oriented(console) as print_stdout:
for address, artifact in addresses_with_provide_artifacts.items():
print_stdout(" ".join(extractor(address, artifact) for extractor in extractors))
return List(exit_code=0)

if documented_enabled:
targets = await Get[Targets](Addresses, addresses)
addresses_with_descriptions = cast(
Dict[Address, str],
{
tgt.address: tgt[DescriptionField].value
for tgt in targets
if tgt.get(DescriptionField).value is not None
},
)
with options.line_oriented(console) as print_stdout:
for address, description in addresses_with_descriptions.items():
formatted_description = "\n ".join(description.strip().split("\n"))
print_stdout(f"{address.spec}\n {formatted_description}")
return List(exit_code=0)

with options.line_oriented(console) as print_stdout:
for address in sorted(addresses):
print_stdout(address)
)


AddressesPrintFunction = Callable[[Address], str]


class List(Goal):
subsystem_cls = ListOptions


@goal_rule
async def list_targets(console: Console, list_options: ListOptions, addresses: Addresses) -> List:
provides = list_options.values.provides
provides_columns = list_options.values.provides_columns
documented = list_options.values.documented
collection: Union[Targets, Addresses, FingerprintedTargetCollection]
print_fn: Union[PrintFunction, FingerprintedPrintFunction, AddressesPrintFunction]

output_format = list_options.values.output_format

# TODO: Remove when these options have completed their deprecation cycle!
if provides:
output_format = ListOptions.OutputFormat.provides
elif documented:
output_format = ListOptions.OutputFormat.documented

# TODO: a match() method for Enums which allows `await Get()` within it somehow!
if output_format == ListOptions.OutputFormat.provides:
# To get provides clauses, we need hydrated targets.
collection = await Get[Targets](Addresses, addresses)
print_fn = _make_provides_print_fn(provides_columns, collection)
elif output_format == ListOptions.OutputFormat.documented:
# To get documentation, we need hydrated targets.
collection = await Get[Targets](Addresses, addresses)
print_fn = _make_print_documented_target(collection)
elif output_format == ListOptions.OutputFormat.json:
# To get fingerprints of each target and its dependencies, we have to request that information
# specifically.
collection = await Get[FingerprintedTargetCollection](Addresses, addresses)
print_fn = _print_fingerprinted_target
else:
assert output_format == ListOptions.OutputFormat.address_specs
# Otherwise, we can use only addresses.
collection = addresses
print_fn = lambda address: address.spec

with list_options.line_oriented(console) as print_stdout:
if not collection.dependencies:
console.print_stderr("WARNING: No targets were matched in goal `{}`.".format("list"))

for item in collection:
# The above waterfall of `if` conditionals using the ListOptions.OutputFormat enum
# should ensure that the types of `collection` and `print_fn` are matched up.
result = print_fn(item) # type: ignore[arg-type]
if result:
print_stdout(result)

return List(exit_code=0)


Expand Down
11 changes: 9 additions & 2 deletions src/python/pants/core/project_info/list_targets_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from textwrap import dedent
from typing import List, Optional, Tuple, cast
from typing import Dict, List, Optional, Tuple, cast

from pants.backend.jvm.artifact import Artifact
from pants.backend.jvm.repository import Repository
from pants.core.project_info.list_targets import ListOptions, list_targets
from pants.engine.addresses import Address, Addresses
from pants.engine.legacy.graph import FingerprintedTargetCollection, TransitiveFingerprintedTarget
from pants.engine.target import DescriptionField, ProvidesField, Target, Targets
from pants.testutil.engine.util import MockConsole, MockGet, create_goal_subsystem, run_rule

Expand All @@ -20,6 +21,7 @@ class MockTarget(Target):
def run_goal(
targets: List[MockTarget],
*,
target_fingerprints: Dict[Address, TransitiveFingerprintedTarget] = {},
show_documented: bool = False,
show_provides: bool = False,
provides_columns: Optional[str] = None,
Expand All @@ -39,7 +41,12 @@ def run_goal(
),
console,
],
mock_gets=[MockGet(product_type=Targets, subject_type=Addresses, mock=lambda _: targets)],
mock_gets=[
MockGet(product_type=Targets, subject_type=Addresses, mock=lambda _: targets),
MockGet(product_type=FingerprintedTargetCollection, subject_type=Addresses,
mock=lambda addresses: FingerprintedTargetCollection(target_fingerprints[addr]
for addr in addresses))
],
)
return cast(str, console.stdout.getvalue()), cast(str, console.stderr.getvalue())

Expand Down
Loading