Skip to content

Commit

Permalink
Have peek emit expanded sources and dependencies (pantsbuild#12882)
Browse files Browse the repository at this point in the history
This will allow it to be used to produce a detailed JSON dependency graph that can be consumed outside Pants.

[ci skip-rust]

[ci skip-build-wheels]
  • Loading branch information
benjyw authored Sep 30, 2021
1 parent 761533e commit 8ba0dfa
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 70 deletions.
115 changes: 84 additions & 31 deletions src/python/pants/backend/project_info/peek.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,43 @@
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import collections.abc
from __future__ import annotations

import collections
import json
import os
from dataclasses import asdict, is_dataclass
from dataclasses import asdict, dataclass, is_dataclass
from enum import Enum
from typing import Any, Iterable, Mapping, cast

from pkg_resources import Requirement

from pants.engine.addresses import Address, BuildFileAddress
from pants.engine.collection import Collection
from pants.engine.console import Console
from pants.engine.fs import DigestContents, FileContent, PathGlobs
from pants.engine.goal import Goal, GoalSubsystem, Outputting
from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule
from pants.engine.target import Target, UnexpandedTargets


from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule
from pants.engine.target import (
Dependencies,
DependenciesRequest,
HydratedSources,
HydrateSourcesRequest,
Sources,
Target,
Targets,
UnexpandedTargets,
)


# TODO: Delete this in 2.9.0.dev0.
class OutputOptions(Enum):
RAW = "raw"
JSON = "json"


class PeekSubsystem(Outputting, GoalSubsystem):
"""Display BUILD file info to the console.
In its most basic form, `peek` just prints the contents of a BUILD file. It can also display
multiple BUILD files, or render normalized target metadata as JSON for consumption by other
programs.
"""
"""Display detailed target information in JSON form."""

name = "peek"
help = "Display BUILD target info"
Expand All @@ -41,6 +49,9 @@ def register_options(cls, register):
"--output",
type=OutputOptions,
default=OutputOptions.JSON,
removal_version="2.9.0.dev0",
removal_hint="Output will always be JSON. If you need the raw BUILD file contents, "
"look at it directly!",
help=(
"Which output style peek should use: `json` will show each target as a seperate "
"entry, whereas `raw` will simply show the original non-normalized BUILD files."
Expand All @@ -50,12 +61,10 @@ def register_options(cls, register):
"--exclude-defaults",
type=bool,
default=False,
help=(
"Whether to leave off values that match the target-defined default values "
"when using `json` output."
),
help="Whether to leave off values that match the target-defined default values.",
)

# TODO: Delete this in 2.9.0.dev0.
@property
def output_type(self) -> OutputOptions:
"""Get the output type from options.
Expand All @@ -73,12 +82,14 @@ class Peek(Goal):
subsystem_cls = PeekSubsystem


# TODO: Delete this in 2.9.0.dev0.
def _render_raw(fcs: Iterable[FileContent]) -> str:
sorted_fcs = sorted(fcs, key=lambda fc: fc.path)
rendereds = map(_render_raw_build_file, sorted_fcs)
return os.linesep.join(rendereds)


# TODO: Delete this in 2.9.0.dev0.
def _render_raw_build_file(fc: FileContent, encoding: str = "utf-8") -> str:
dashes = "-" * len(fc.path)
content = fc.content.decode(encoding)
Expand All @@ -88,27 +99,39 @@ def _render_raw_build_file(fc: FileContent, encoding: str = "utf-8") -> str:
return os.linesep.join(parts)


_nothing = object()
@dataclass(frozen=True)
class TargetData:
target: Target
expanded_sources: tuple[str, ...] | None # Target may not be of a type that has sources.
expanded_dependencies: tuple[str, ...]


class TargetDatas(Collection[TargetData]):
pass


def _render_json(ts: Iterable[Target], exclude_defaults: bool = False) -> str:
def _render_json(tds: Iterable[TargetData], exclude_defaults: bool = False) -> str:
nothing = object()

targets: Iterable[Mapping[str, Any]] = [
{
"address": t.address.spec,
"target_type": t.alias,
"address": td.target.address.spec,
"target_type": td.target.alias,
**{
k.alias: v.value
for k, v in t.field_values.items()
if not (exclude_defaults and getattr(k, "default", _nothing) == v.value)
(f"{k.alias}_raw" if k.alias in {"sources", "dependencies"} else k.alias): v.value
for k, v in td.target.field_values.items()
if not (exclude_defaults and getattr(k, "default", nothing) == v.value)
},
**({} if td.expanded_sources is None else {"sources": td.expanded_sources}),
"dependencies": td.expanded_dependencies,
}
for t in ts
for td in tds
]
return f"{json.dumps(targets, indent=2, cls=_PeekJsonEncoder)}\n"


class _PeekJsonEncoder(json.JSONEncoder):
"""Allow us to serialize some commmonly-found types in BUILD files."""
"""Allow us to serialize some commmonly found types in BUILD files."""

safe_to_str_types = (Requirement,)

Expand All @@ -126,27 +149,57 @@ def default(self, o):
return str(o)


@rule
async def get_target_data(targets: UnexpandedTargets) -> TargetDatas:
sorted_targets = sorted(targets, key=lambda tgt: tgt.address)

dependencies_per_target = await MultiGet(
Get(
Targets,
DependenciesRequest(tgt.get(Dependencies), include_special_cased_deps=True),
)
for tgt in sorted_targets
)

# Not all targets have a sources field, so we have to do a dance here.
targets_with_sources = [tgt for tgt in sorted_targets if tgt.has_field(Sources)]
all_hydrated_sources = await MultiGet(
Get(HydratedSources, HydrateSourcesRequest(tgt[Sources])) for tgt in targets_with_sources
)
hydrated_sources_map = {
tgt.address: hs for tgt, hs in zip(targets_with_sources, all_hydrated_sources)
}
sources_per_target = [hydrated_sources_map.get(tgt.address) for tgt in sorted_targets]

return TargetDatas(
TargetData(
tgt, srcs.snapshot.files if srcs else None, tuple(dep.address.spec for dep in deps)
)
for tgt, srcs, deps in zip(sorted_targets, sources_per_target, dependencies_per_target)
)


@goal_rule
async def peek(
console: Console,
subsys: PeekSubsystem,
targets: UnexpandedTargets,
) -> Peek:
# TODO: Delete this entire conditional in 2.9.0.dev0.
if subsys.output_type == OutputOptions.RAW:
build_file_addresses = await MultiGet(
Get(BuildFileAddress, Address, t.address) for t in targets
)
build_file_paths = {a.rel_path for a in build_file_addresses}
digest_contents = await Get(DigestContents, PathGlobs(build_file_paths))
output = _render_raw(digest_contents)
elif subsys.output_type == OutputOptions.JSON:
output = _render_json(targets, subsys.exclude_defaults)
else:
raise AssertionError(f"output_type not one of {tuple(OutputOptions)}")
with subsys.output(console) as write_stdout:
write_stdout(_render_raw(digest_contents))
return Peek(exit_code=0)

tds = await Get(TargetDatas, UnexpandedTargets, targets)
output = _render_json(tds, subsys.exclude_defaults)
with subsys.output(console) as write_stdout:
write_stdout(output)

return Peek(exit_code=0)


Expand Down
Loading

0 comments on commit 8ba0dfa

Please sign in to comment.