Skip to content

Commit

Permalink
add --with-fingerprints to list
Browse files Browse the repository at this point in the history
add --with-fingerprints to list

coerce the provides key when fingerprinting targets

coerce the provides key when fingerprinting targets

convert the option to be named --output-format!

ensure fingerprints incorporate sources snapshots

use the new Enum type!

make fingerprints easier to create

clean up impl

bump deprecation version

fix ci

[ci skip-rust-tests]  # No Rust changes made.

[ci skip-jvm-tests]  # No JVM changes made.
  • Loading branch information
cosmicexplorer committed May 4, 2020
1 parent 12de11e commit 66a7bed
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 94 deletions.
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 --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

0 comments on commit 66a7bed

Please sign in to comment.