Skip to content

Commit

Permalink
Improvements and refactoring of help system.
Browse files Browse the repository at this point in the history
[ci skip-rust-tests]
  • Loading branch information
benjyw committed Jul 7, 2020
1 parent 4b09cdc commit c669642
Show file tree
Hide file tree
Showing 11 changed files with 515 additions and 324 deletions.
10 changes: 9 additions & 1 deletion src/python/pants/bin/local_pants_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pants.engine.internals.scheduler import ExecutionError
from pants.engine.unions import UnionMembership
from pants.goal.run_tracker import RunTracker
from pants.help.help_info_extracter import HelpInfoExtracter
from pants.help.help_printer import HelpPrinter
from pants.init.engine_initializer import (
EngineInitializer,
Expand Down Expand Up @@ -275,8 +276,15 @@ def run(self, start_time: float) -> ExitCode:
)

if self.options.help_request:
all_help_info = HelpInfoExtracter.get_all_help_info(
self.options,
self.union_membership,
self.graph_session.goal_consumed_subsystem_scopes,
)
help_printer = HelpPrinter(
options=self.options, union_membership=self.union_membership
bin_name=self.options.for_global_scope().pants_bin_name,
help_request=self.options.help_request,
all_help_info=all_help_info,
)
return help_printer.print_help()

Expand Down
24 changes: 7 additions & 17 deletions src/python/pants/help/help_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@

from colors import cyan, green, magenta, red

from pants.help.help_info_extracter import HelpInfoExtracter, OptionHelpInfo
from pants.help.help_info_extracter import OptionHelpInfo, OptionScopeHelpInfo


class HelpFormatter:
def __init__(
self, *, scope: str, show_advanced: bool, show_deprecated: bool, color: bool
) -> None:
self._scope = scope
def __init__(self, *, show_advanced: bool, show_deprecated: bool, color: bool) -> None:
self._show_advanced = show_advanced
self._show_deprecated = show_deprecated
self._color = color
Expand All @@ -33,28 +30,21 @@ def _maybe_magenta(self, s):
def _maybe_color(self, color, s):
return color(s) if self._color else s

def format_options(self, scope, description, option_registrations_iter):
"""Return a help message for the specified options.
:param scope: The options scope.
:param description: The description of the scope.
:param option_registrations_iter: An iterator over (args, kwargs) pairs, as passed in to
options registration.
"""
oshi = HelpInfoExtracter(self._scope).get_option_scope_help_info(option_registrations_iter)
def format_options(self, oshi: OptionScopeHelpInfo):
"""Return a help message for the specified options."""
lines = []

def add_option(ohis, *, category=None):
lines.append("")
display_scope = f"`{scope}`" if scope else "Global"
display_scope = f"`{oshi.scope}`" if oshi.scope else "Global"
if category:
title = f"{display_scope} {category} options"
lines.append(self._maybe_green(f"{title}\n{'-' * len(title)}"))
else:
title = f"{display_scope} options"
lines.append(self._maybe_green(f"{title}\n{'-' * len(title)}"))
if description:
lines.append(f"\n{description}")
if oshi.description:
lines.append(f"\n{oshi.description}")
lines.append(" ")
if not ohis:
lines.append("No options available.")
Expand Down
42 changes: 21 additions & 21 deletions src/python/pants/help/help_formatter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
from dataclasses import replace

from pants.help.help_formatter import HelpFormatter
from pants.help.help_info_extracter import OptionHelpInfo
from pants.help.help_info_extracter import HelpInfoExtracter, OptionHelpInfo


class OptionHelpFormatterTest(unittest.TestCase):
def format_help_for_foo(self, **kwargs):
@staticmethod
def _format_for_single_option(**kwargs):
ohi = OptionHelpInfo(
registering_class=type(None),
display_args=["--foo"],
display_args=("--foo",),
comma_separated_display_args="--foo",
scoped_cmd_line_args=["--foo"],
unscoped_cmd_line_args=["--foo"],
scoped_cmd_line_args=tuple("--foo",),
unscoped_cmd_line_args=tuple("--foo",),
typ=bool,
default=None,
default_str="None",
help="help for foo",
deprecated_message=None,
removal_version=None,
Expand All @@ -26,44 +27,43 @@ def format_help_for_foo(self, **kwargs):
)
ohi = replace(ohi, **kwargs)
lines = HelpFormatter(
scope="", show_advanced=False, show_deprecated=False, color=False
show_advanced=False, show_deprecated=False, color=False
).format_option(ohi)
assert len(lines) == 3
assert "help for foo" in lines[2]
return lines[1]

def test_format_help(self):
default_line = self.format_help_for_foo(default="MYDEFAULT")
default_line = self._format_for_single_option(default="MYDEFAULT")
assert default_line.lstrip() == "default: MYDEFAULT"

def test_format_help_choices(self):
default_line = self.format_help_for_foo(
default_line = self._format_for_single_option(
typ=str, default="kiwi", choices="apple, banana, kiwi"
)
assert default_line.lstrip() == "one of: [apple, banana, kiwi]; default: kiwi"

@staticmethod
def _format_for_global_scope(show_advanced, show_deprecated, args, kwargs):
oshi = HelpInfoExtracter("").get_option_scope_help_info("", [(args, kwargs)])
return HelpFormatter(
show_advanced=show_advanced, show_deprecated=show_deprecated, color=False
).format_options(oshi)

def test_suppress_advanced(self):
args = ["--foo"]
kwargs = {"advanced": True}
lines = HelpFormatter(
scope="", show_advanced=False, show_deprecated=False, color=False
).format_options(scope="", description="", option_registrations_iter=[(args, kwargs)])
lines = self._format_for_global_scope(False, False, args, kwargs)
assert len(lines) == 5
assert not any("--foo" in line for line in lines)
lines = HelpFormatter(
scope="", show_advanced=True, show_deprecated=False, color=False
).format_options(scope="", description="", option_registrations_iter=[(args, kwargs)])
lines = self._format_for_global_scope(True, False, args, kwargs)
assert len(lines) == 12

def test_suppress_deprecated(self):
args = ["--foo"]
kwargs = {"removal_version": "33.44.55"}
lines = HelpFormatter(
scope="", show_advanced=False, show_deprecated=False, color=False
).format_options(scope="", description="", option_registrations_iter=[(args, kwargs)])
lines = self._format_for_global_scope(False, False, args, kwargs)
assert len(lines) == 5
assert not any("--foo" in line for line in lines)
lines = HelpFormatter(
scope="", show_advanced=True, show_deprecated=True, color=False
).format_options(scope="", description="", option_registrations_iter=[(args, kwargs)])
lines = self._format_for_global_scope(True, True, args, kwargs)
assert len(lines) == 17
113 changes: 83 additions & 30 deletions src/python/pants/help/help_info_extracter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
import inspect
from dataclasses import dataclass
from enum import Enum
from typing import List, Optional, Type
from typing import Any, Callable, Dict, Optional, Tuple, Type

from pants.base import deprecated
from pants.engine.goal import GoalSubsystem
from pants.engine.unions import UnionMembership
from pants.option.option_util import is_dict_option, is_list_option
from pants.option.options import Options


@dataclass(frozen=True)
class OptionHelpInfo:
"""A container for help information for a single option.
registering_class: The type that registered the option.
display_args: Arg strings suitable for display in help text, including value examples
(e.g., [-f, --[no]-foo-bar, --baz=<metavar>].)
comma_separated_display_args: Display args as a comma-delimited string, used in
Expand All @@ -33,13 +35,13 @@ class OptionHelpInfo:
choices: If this option has a constrained list of choices, a csv list of the choices.
"""

registering_class: Type
display_args: List[str]
display_args: Tuple[str, ...]
comma_separated_display_args: str
scoped_cmd_line_args: List[str]
unscoped_cmd_line_args: List[str]
scoped_cmd_line_args: Tuple[str, ...]
unscoped_cmd_line_args: Tuple[str, ...]
typ: Type
default: str
default: Any
default_str: str
help: str
deprecated_message: Optional[str]
removal_version: Optional[str]
Expand All @@ -56,31 +58,81 @@ class OptionScopeHelpInfo:
"""

scope: str
basic: List[OptionHelpInfo]
advanced: List[OptionHelpInfo]
deprecated: List[OptionHelpInfo]
description: str
basic: Tuple[OptionHelpInfo, ...]
advanced: Tuple[OptionHelpInfo, ...]
deprecated: Tuple[OptionHelpInfo, ...]


@dataclass(frozen=True)
class GoalHelpInfo:
"""A container for help information for a goal."""

name: str
description: str
is_implemented: bool # True iff all unions required by the goal are implemented.
consumed_scopes: Tuple[str, ...] # The scopes of subsystems consumed by this goal.


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

scope_to_help_info: Dict[str, OptionScopeHelpInfo]
name_to_goal_info: Dict[str, GoalHelpInfo]


UsedScopesMapper = Callable[[str], Tuple[str, ...]]


class HelpInfoExtracter:
"""Extracts information useful for displaying help from option registration args."""

@classmethod
def get_option_scope_help_info_from_parser(cls, parser):
"""Returns a dict of help information for the options registered on the given parser.
Callers can format this dict into cmd-line help, HTML or whatever.
"""
return cls(parser.scope).get_option_scope_help_info(parser.option_registrations_iter())
def get_all_help_info(
cls,
options: Options,
union_membership: UnionMembership,
used_scopes_mapper: UsedScopesMapper,
) -> AllHelpInfo:
scope_to_help_info = {}
name_to_goal_info = {}
for scope_info in sorted(options.known_scope_to_info.values(), key=lambda x: x.scope):
oshi: OptionScopeHelpInfo = HelpInfoExtracter(
scope_info.scope
).get_option_scope_help_info(
scope_info.description,
options.get_parser(scope_info.scope).option_registrations_iter(),
)
scope_to_help_info[oshi.scope] = oshi

if issubclass(scope_info.optionable_cls, GoalSubsystem):
is_implemented = union_membership.has_members_for_all(
scope_info.optionable_cls.required_union_implementations
)
name_to_goal_info[scope_info.scope] = GoalHelpInfo(
scope_info.optionable_cls.name,
scope_info.description,
is_implemented,
used_scopes_mapper(scope_info.scope),
)

return AllHelpInfo(
scope_to_help_info=scope_to_help_info, name_to_goal_info=name_to_goal_info
)

@staticmethod
def compute_default(kwargs) -> str:
"""Compute the default val for help display for an option registered with these kwargs."""
def compute_default(**kwargs) -> Tuple[Any, str]:
"""Compute the default val for help display for an option registered with these kwargs.
Returns a pair (default, stringified default suitable for display).
"""
ranked_default = kwargs.get("default")
typ = kwargs.get("type", str)

default = ranked_default.value if ranked_default else None
if default is None:
return "None"
return None, "None"

if is_list_option(kwargs):
member_type = kwargs.get("member_type", str)
Expand All @@ -105,7 +157,7 @@ def member_str(val):
default_str = default.value
else:
default_str = str(default)
return default_str
return default, default_str

@staticmethod
def stringify_type(t: Type) -> str:
Expand Down Expand Up @@ -150,11 +202,11 @@ def compute_choices(kwargs) -> Optional[str]:
values = (str(choice) for choice in kwargs.get("choices", []))
return ", ".join(values) or None

def __init__(self, scope):
def __init__(self, scope: str):
self._scope = scope
self._scope_prefix = scope.replace(".", "-")

def get_option_scope_help_info(self, option_registrations_iter):
def get_option_scope_help_info(self, description, option_registrations_iter):
"""Returns an OptionScopeHelpInfo for the options registered with the (args, kwargs)
pairs."""
basic_options = []
Expand All @@ -179,9 +231,10 @@ def get_option_scope_help_info(self, option_registrations_iter):

return OptionScopeHelpInfo(
scope=self._scope,
basic=basic_options,
advanced=advanced_options,
deprecated=deprecated_options,
description=description,
basic=tuple(basic_options),
advanced=tuple(advanced_options),
deprecated=tuple(deprecated_options),
)

def get_option_help_info(self, args, kwargs):
Expand Down Expand Up @@ -215,7 +268,7 @@ def get_option_help_info(self, args, kwargs):
display_args.append(f"... -- [{type_str} [{type_str} [...]]]")

typ = kwargs.get("type", str)
default = self.compute_default(kwargs)
default, default_str = self.compute_default(**kwargs)
help_msg = kwargs.get("help", "No help available.")
removal_version = kwargs.get("removal_version")
deprecated_message = None
Expand All @@ -228,13 +281,13 @@ def get_option_help_info(self, args, kwargs):
choices = self.compute_choices(kwargs)

ret = OptionHelpInfo(
registering_class=kwargs.get("registering_class", type(None)),
display_args=display_args,
display_args=tuple(display_args),
comma_separated_display_args=", ".join(display_args),
scoped_cmd_line_args=scoped_cmd_line_args,
unscoped_cmd_line_args=unscoped_cmd_line_args,
scoped_cmd_line_args=tuple(scoped_cmd_line_args),
unscoped_cmd_line_args=tuple(unscoped_cmd_line_args),
typ=typ,
default=default,
default_str=default_str,
help=help_msg,
deprecated_message=deprecated_message,
removal_version=removal_version,
Expand Down

0 comments on commit c669642

Please sign in to comment.