Skip to content

Commit

Permalink
Add new api-types help goal (#14081)
Browse files Browse the repository at this point in the history
Add help info for all available rule output types, and the rules that provides them. (except those prefixed with `_`).

    $ ./pants help api-types
  • Loading branch information
kaos committed Jan 17, 2022
1 parent 2e126e7 commit 33e341a
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 2 deletions.
Expand Up @@ -11,6 +11,7 @@ class ListAndDieForTestingSubsystem(GoalSubsystem):
"""A fast and deadly variant of `./pants list`."""

name = "list-and-die-for-testing"
help = "A fast and deadly variant of `./pants list`."


class ListAndDieForTesting(Goal):
Expand Down
2 changes: 1 addition & 1 deletion src/python/pants/build_graph/build_configuration.py
Expand Up @@ -28,7 +28,7 @@

# No goal or target_type can have a name from this set, so that `./pants help <name>`
# is unambiguous.
_RESERVED_NAMES = {"global", "goals", "targets", "tools"}
_RESERVED_NAMES = {"api-types", "global", "goals", "targets", "tools"}


# Subsystems used outside of any rule.
Expand Down
52 changes: 52 additions & 0 deletions src/python/pants/help/help_info_extracter.py
Expand Up @@ -8,11 +8,13 @@
import json
from dataclasses import dataclass
from enum import Enum
from itertools import groupby
from typing import Any, Callable, Tuple, Type, Union, cast, get_type_hints

from pants.base import deprecated
from pants.build_graph.build_configuration import BuildConfiguration
from pants.engine.goal import GoalSubsystem
from pants.engine.rules import TaskRule
from pants.engine.target import Field, RegisteredTargetTypes, StringField, Target
from pants.engine.unions import UnionMembership, UnionRule
from pants.option.option_util import is_dict_option, is_list_option
Expand Down Expand Up @@ -228,13 +230,26 @@ def create(
)


@dataclass(frozen=True)
class RuleInfo:
name: str
description: str | None
help: str | None
provider: str
input_types: tuple[str, ...]
input_gets: tuple[str, ...]
output_type: str
output_desc: str | None


@dataclass(frozen=True)
class AllHelpInfo:
"""All available help info."""

scope_to_help_info: dict[str, OptionScopeHelpInfo]
name_to_goal_info: dict[str, GoalHelpInfo]
name_to_target_type_info: dict[str, TargetTypeHelpInfo]
rule_output_type_to_rule_infos: dict[str, tuple[RuleInfo, ...]]


ConsumedScopesMapper = Callable[[str], Tuple[str, ...]]
Expand Down Expand Up @@ -323,6 +338,7 @@ def get_all_help_info(
scope_to_help_info=scope_to_help_info,
name_to_goal_info=name_to_goal_info,
name_to_target_type_info=name_to_target_type_info,
rule_output_type_to_rule_infos=cls.get_rule_infos(build_configuration),
)

@staticmethod
Expand Down Expand Up @@ -397,6 +413,42 @@ def get_first_provider(providers: tuple[str, ...] | None) -> str:
return ""
return providers[0]

@staticmethod
def maybe_cleandoc(doc: str | None) -> str | None:
return doc and inspect.cleandoc(doc)

@staticmethod
def rule_info_output_type(rule_info: RuleInfo) -> str:
return rule_info.output_type

@classmethod
def get_rule_infos(
cls, build_configuration: BuildConfiguration | None
) -> dict[str, tuple[RuleInfo, ...]]:
if build_configuration is None:
return {}

rule_infos = [
RuleInfo(
name=rule.canonical_name,
description=rule.desc,
help=cls.maybe_cleandoc(rule.func.__doc__),
provider=cls.get_first_provider(providers),
input_types=tuple(selector.__name__ for selector in rule.input_selectors),
input_gets=tuple(str(constraints) for constraints in rule.input_gets),
output_type=rule.output_type.__name__,
output_desc=cls.maybe_cleandoc(rule.output_type.__doc__),
)
for rule, providers in build_configuration.rule_to_providers.items()
if isinstance(rule, TaskRule)
]
return {
rule_output_type: tuple(infos)
for rule_output_type, infos in groupby(
sorted(rule_infos, key=cls.rule_info_output_type), key=cls.rule_info_output_type
)
}

def __init__(self, scope: str):
self._scope = scope
self._scope_prefix = scope.replace(".", "-")
Expand Down
38 changes: 38 additions & 0 deletions src/python/pants/help/help_info_extracter_test.py
Expand Up @@ -7,6 +7,7 @@

from pants.build_graph.build_configuration import BuildConfiguration
from pants.engine.goal import GoalSubsystem
from pants.engine.rules import collect_rules, rule
from pants.engine.target import IntField, RegisteredTargetTypes, StringField, Target
from pants.engine.unions import UnionMembership
from pants.help.help_info_extracter import HelpInfoExtracter, pretty_print_type_hint, to_help_str
Expand Down Expand Up @@ -272,12 +273,17 @@ class BazLibrary(Target):
Foo.register_options_on_scope(options)
Bar.register_options_on_scope(options)

@rule
def rule_info_test(foo: Foo) -> Target:
"""This rule is for testing info extraction only."""

def fake_consumed_scopes_mapper(scope: str) -> Tuple[str, ...]:
return ("somescope", f"used_by_{scope or 'GLOBAL_SCOPE'}")

bc_builder = BuildConfiguration.Builder()
bc_builder.register_subsystems("help_info_extracter_test", (Foo, Bar))
bc_builder.register_target_types("help_info_extracter_test", (BazLibrary,))
bc_builder.register_rules("help_info_extracter_test", collect_rules(locals()))

all_help_info = HelpInfoExtracter.get_all_help_info(
options,
Expand Down Expand Up @@ -386,6 +392,38 @@ def fake_consumed_scopes_mapper(scope: str) -> Tuple[str, ...]:
"deprecated": tuple(),
},
},
"rule_output_type_to_rule_infos": {
"Foo": (
{
"description": None,
"help": "A foo.",
"input_gets": ("Get(ScopedOptions, Scope, ..)",),
"input_types": (),
"name": "construct_scope_foo",
"output_desc": None,
"output_type": "Foo",
"provider": "help_info_extracter_test",
},
),
"Target": (
{
"description": None,
"help": "This rule is for testing info extraction only.",
"input_gets": (),
"input_types": ("Foo",),
"name": "pants.help.help_info_extracter_test.rule_info_test",
"output_desc": (
"A Target represents an addressable set of metadata.\n\n Set the "
"`help` class property with a description, which will be used in "
"`./pants help`. For the\n best rendering, use soft wrapping (e.g. "
"implicit string concatenation) within paragraphs, but\n hard wrapping "
"(`\n`) to separate distinct paragraphs and/or lists.\n "
),
"output_type": "Target",
"provider": "help_info_extracter_test",
},
),
},
"name_to_goal_info": {
"bar": {
"name": "bar",
Expand Down
58 changes: 57 additions & 1 deletion src/python/pants/help/help_printer.py
Expand Up @@ -24,7 +24,7 @@
)
from pants.option.scope import GLOBAL_SCOPE
from pants.util.docutil import terminal_width
from pants.util.strutil import first_paragraph, hard_wrap
from pants.util.strutil import first_paragraph, hard_wrap, pluralize


class HelpPrinter(MaybeColor):
Expand Down Expand Up @@ -110,10 +110,14 @@ def _print_thing_help(self) -> None:
self._print_options_help(GLOBAL_SCOPE, help_request.advanced)
elif thing == "tools":
self._print_all_tools()
elif thing == "api-types":
self._print_all_api_types()
elif thing in self._all_help_info.scope_to_help_info:
self._print_options_help(thing, help_request.advanced)
elif thing in self._all_help_info.name_to_target_type_info:
self._print_target_help(thing)
elif thing in self._all_help_info.rule_output_type_to_rule_infos:
self._print_api_type_help(thing, help_request.advanced)
else:
print(self.maybe_red(f"Unknown entity: {thing}"))
else:
Expand Down Expand Up @@ -198,6 +202,22 @@ def _print_all_tools(self) -> None:
tool_help_cmd = f"{self._bin_name} help $tool"
print(f"Use `{self.maybe_green(tool_help_cmd)}` to get help for a specific tool.\n")

def _print_all_api_types(self) -> None:
self._print_title("Plugin API Types")
api_type_descriptions: Dict[str, str] = {}
for api_type, rule_infos in self._all_help_info.rule_output_type_to_rule_infos.items():
if api_type.startswith("_"):
continue
api_type_descriptions[api_type] = rule_infos[0].output_desc or ""
longest_api_type_name = max(len(name) for name in api_type_descriptions.keys())
chars_before_description = longest_api_type_name + 2
for api_type, description in api_type_descriptions.items():
name = self.maybe_cyan(api_type.ljust(chars_before_description))
description = self._format_summary_description(description, chars_before_description)
print(f"{name}{description}\n")
api_help_cmd = f"{self._bin_name} help $api_type"
print(f"Use `{self.maybe_green(api_help_cmd)}` to get help for a specific API type.\n")

def _print_global_help(self):
def print_cmd(args: str, desc: str):
cmd = self.maybe_green(f"{self._bin_name} {args}".ljust(50))
Expand Down Expand Up @@ -292,6 +312,42 @@ def _print_target_help(self, target_alias: str) -> None:
print("\n" + formatted_desc)
print()

def _print_api_type_help(self, output_type: str, show_advanced: bool) -> None:
self._print_title(f"`{output_type}` API type")
rule_infos = self._all_help_info.rule_output_type_to_rule_infos[output_type]
if rule_infos[0].output_desc:
print("\n".join(hard_wrap(rule_infos[0].output_desc, width=self._width)))
print()
print(f"Returned by {pluralize(len(rule_infos), 'rule')}:")
for rule_info in rule_infos:
print()
print(self.maybe_magenta(rule_info.name))
indent = " "
print(self.maybe_cyan(f"{indent}activated by"), rule_info.provider)
if rule_info.input_types:
print(
self.maybe_cyan(f"{indent}{pluralize(len(rule_info.input_types), 'input')}:"),
", ".join(rule_info.input_types),
)
else:
print(self.maybe_cyan(f"{indent}no inputs"))
if show_advanced and rule_info.input_gets:
print(
f"\n{indent}".join(
hard_wrap(
self.maybe_cyan(f"{pluralize(len(rule_info.input_gets), 'get')}: ")
+ ", ".join(rule_info.input_gets),
indent=4,
width=self._width - 4,
)
)
)
if rule_info.description:
print(f"{indent}{rule_info.description}")
if rule_info.help:
print("\n" + "\n".join(hard_wrap(rule_info.help, indent=4, width=self._width)))
print()

def _get_help_json(self) -> str:
"""Return a JSON object containing all the help info we have, for every scope."""
return json.dumps(
Expand Down
1 change: 1 addition & 0 deletions src/python/pants/jvm/resolve/jvm_tool_test.py
Expand Up @@ -23,6 +23,7 @@

class MockJvmTool(JvmToolBase):
options_scope = "mock-tool"
help = "Hamcrest is a mocking tool for the JVM."

default_version = "1.3"
default_artifacts = ("org.hamcrest:hamcrest-core:{version}",)
Expand Down
1 change: 1 addition & 0 deletions src/python/pants/option/subsystem.py
Expand Up @@ -53,6 +53,7 @@ def signature(cls):
name = f"construct_scope_{snake_scope}"
partial_construct_subsystem.__name__ = name
partial_construct_subsystem.__module__ = cls.__module__
partial_construct_subsystem.__doc__ = cls.help
_, class_definition_lineno = inspect.getsourcelines(cls)
partial_construct_subsystem.__line_number__ = class_definition_lineno

Expand Down

0 comments on commit 33e341a

Please sign in to comment.