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

Integrate option value derivation into help. #10313

Merged
merged 6 commits into from
Jul 11, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/python/pants/backend/pants_info/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@


def rules():
return [*list_target_types.rules()]
return list_target_types.rules()
2 changes: 1 addition & 1 deletion src/python/pants/binaries/binary_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def select(argv):
# Parse positional arguments to the script.
args = _create_bootstrap_binary_arg_parser().parse_args(argv[1:])
# Resolve bootstrap options with a fake empty command line.
options_bootstrapper = OptionsBootstrapper.create(args=[argv[0]])
options_bootstrapper = OptionsBootstrapper.create(env=os.environ, args=[argv[0]])
subsystems = (GlobalOptions, BinaryUtil.Factory)
known_scope_infos = reduce(set.union, (ss.known_scope_infos() for ss in subsystems), set())
options = options_bootstrapper.get_full_options(known_scope_infos)
Expand Down
40 changes: 35 additions & 5 deletions src/python/pants/help/help_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from textwrap import wrap
from typing import List
from typing import List, Optional

from colors import cyan, green, magenta, red

from pants.help.help_info_extracter import OptionHelpInfo, OptionScopeHelpInfo
from pants.option.ranked_value import Rank


class HelpFormatter:
Expand Down Expand Up @@ -65,15 +66,44 @@ def format_option(self, ohi: OptionHelpInfo) -> List[str]:
:param ohi: Extracted information for option to print
:return: Formatted help text for this option
"""

def maybe_parens(s: Optional[str]) -> str:
return f" ({s})" if s else ""

indent = " "
arg_lines = [f" {self._maybe_magenta(args)}" for args in ohi.display_args]
choices = f"one of: [{ohi.choices}]; " if ohi.choices else ""
default_lines = [
choices = "" if ohi.choices is None else f"one of: [{', '.join(ohi.choices)}]"
choices_lines = [
f"{indent}{' ' if i != 0 else ''}{self._maybe_cyan(s)}"
for i, s in enumerate(wrap(f"{choices}default: {ohi.default}", 80))
for i, s in enumerate(wrap(f"{choices}", 80))
]
default_line = self._maybe_cyan(f"{indent}default: {ohi.default}")
if not ohi.value_history:
# Should never happen, but this keeps mypy happy.
raise ValueError("No value history - options not parsed.")
final_val = ohi.value_history.final_value
curr_value_line = self._maybe_cyan(
f"{indent}current value: {ohi.value_history.final_value.value}{maybe_parens(final_val.details)}"
)

interesting_ranked_values = [
rv
for rv in reversed(ohi.value_history.ranked_values)
if rv.rank not in (Rank.NONE, Rank.HARDCODED, final_val.rank)
]
value_derivation_lines = [
self._maybe_cyan(f"{indent} overrode: {rv.value}{maybe_parens(rv.details)}")
for rv in interesting_ranked_values
]
description_lines = [f"{indent}{s}" for s in wrap(ohi.help, 80)]
lines = [*arg_lines, *default_lines, *description_lines]
lines = [
*arg_lines,
*choices_lines,
default_line,
curr_value_line,
*value_derivation_lines,
*description_lines,
]
if ohi.deprecated_message:
lines.append(self._maybe_red(f"{indent}{ohi.deprecated_message}."))
if ohi.removal_hint:
Expand Down
36 changes: 27 additions & 9 deletions src/python/pants/help/help_formatter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

from pants.help.help_formatter import HelpFormatter
from pants.help.help_info_extracter import HelpInfoExtracter, OptionHelpInfo
from pants.option.config import Config
from pants.option.global_options import GlobalOptions
from pants.option.option_value_container import OptionValueContainer
from pants.option.parser import OptionValueHistory, Parser
from pants.option.ranked_value import Rank, RankedValue


class OptionHelpFormatterTest(unittest.TestCase):
Expand All @@ -24,28 +29,41 @@ def _format_for_single_option(**kwargs):
removal_version=None,
removal_hint=None,
choices=None,
value_history=OptionValueHistory((RankedValue(Rank.HARDCODED, None),)),
)
ohi = replace(ohi, **kwargs)
lines = HelpFormatter(
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]
choices = kwargs.get("choices")
assert len(lines) == 5 if choices else 4
if choices:
assert f"one of: [{', '.join(choices)}]" == lines[1].strip()
assert "help for foo" in lines[4 if choices else 3]
return lines[2] if choices else lines[1]

def test_format_help(self):
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_for_single_option(
typ=str, default="kiwi", choices="apple, banana, kiwi"
typ=str, default="kiwi", choices=["apple", "banana", "kiwi"]
)
assert default_line.lstrip() == "one of: [apple, banana, kiwi]; default: kiwi"
assert default_line.lstrip() == "default: kiwi"

@staticmethod
def _format_for_global_scope(show_advanced, show_deprecated, args, kwargs):
oshi = HelpInfoExtracter("").get_option_scope_help_info("", [(args, kwargs)])
parser = Parser(
env={},
config=Config.load([]),
scope_info=GlobalOptions.get_scope_info(),
parent_parser=None,
)
parser.register(*args, **kwargs)
# Force a parse to generate the derivation history.
parser.parse_args(Parser.ParseArgsRequest((), OptionValueContainer(), lambda: [], 0, []))
oshi = HelpInfoExtracter("").get_option_scope_help_info("", parser)
return HelpFormatter(
show_advanced=show_advanced, show_deprecated=show_deprecated, color=False
).format_options(oshi)
Expand All @@ -57,13 +75,13 @@ def test_suppress_advanced(self):
assert len(lines) == 5
assert not any("--foo" in line for line in lines)
lines = self._format_for_global_scope(True, False, args, kwargs)
assert len(lines) == 12
assert len(lines) == 13

def test_suppress_deprecated(self):
args = ["--foo"]
kwargs = {"removal_version": "33.44.55"}
kwargs = {"removal_version": "33.44.55.dev0"}
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 = self._format_for_global_scope(True, True, args, kwargs)
assert len(lines) == 17
assert len(lines) == 18
52 changes: 30 additions & 22 deletions src/python/pants/help/help_info_extracter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

import dataclasses
import inspect
from dataclasses import dataclass
from enum import Enum
Expand All @@ -11,6 +12,7 @@
from pants.engine.unions import UnionMembership
from pants.option.option_util import is_dict_option, is_list_option
from pants.option.options import Options
from pants.option.parser import OptionValueHistory, Parser


@dataclass(frozen=True)
Expand All @@ -32,7 +34,7 @@ class OptionHelpInfo:
removal_version.
removal_version: If deprecated: The version at which this option is to be removed.
removal_hint: If deprecated: The removal hint message registered for this option.
choices: If this option has a constrained list of choices, a csv list of the choices.
choices: If this option has a constrained set of choices, a tuple of the stringified choices.
"""

display_args: Tuple[str, ...]
Expand All @@ -46,7 +48,8 @@ class OptionHelpInfo:
deprecated_message: Optional[str]
removal_version: Optional[str]
removal_hint: Optional[str]
choices: Optional[str]
choices: Optional[Tuple[str, ...]]
value_history: Optional[OptionValueHistory]


@dataclass(frozen=True)
Expand Down Expand Up @@ -98,11 +101,11 @@ def get_all_help_info(
scope_to_help_info = {}
name_to_goal_info = {}
for scope_info in sorted(options.known_scope_to_info.values(), key=lambda x: x.scope):
options.for_scope(scope_info.scope) # Force parsing.
oshi: OptionScopeHelpInfo = HelpInfoExtracter(
scope_info.scope
).get_option_scope_help_info(
scope_info.description,
options.get_parser(scope_info.scope).option_registrations_iter(),
scope_info.description, options.get_parser(scope_info.scope),
)
scope_to_help_info[oshi.scope] = oshi

Expand Down Expand Up @@ -130,11 +133,8 @@ def compute_default(**kwargs) -> Tuple[Any, str]:
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, "None"

if is_list_option(kwargs):
default = ranked_default.value if ranked_default else []
member_type = kwargs.get("member_type", str)
if inspect.isclass(member_type) and issubclass(member_type, Enum):
default = []
Expand All @@ -146,17 +146,21 @@ def member_str(val):
f"\"[{', '.join(member_str(val) for val in default)}]\"" if default else "[]"
)
elif is_dict_option(kwargs):
default = ranked_default.value if ranked_default else {}
if default:
items_str = ", ".join(f"'{k}': {v}" for k, v in default.items())
default_str = f"{{ {items_str} }}"
else:
default_str = "{}"
elif typ == str:
default_str = default.replace("\n", " ")
elif inspect.isclass(typ) and issubclass(typ, Enum):
default_str = default.value
else:
default = ranked_default.value if ranked_default else None
default_str = str(default)

if typ == str:
default_str = default_str.replace("\n", " ")
elif isinstance(default, Enum):
default_str = default.value

return default, default_str

@staticmethod
Expand Down Expand Up @@ -190,33 +194,36 @@ def compute_metavar(kwargs):
return metavar

@staticmethod
def compute_choices(kwargs) -> Optional[str]:
"""Compute the option choices to display based on an Enum or list type."""
def compute_choices(kwargs) -> Optional[Tuple[str, ...]]:
"""Compute the option choices to display."""
typ = kwargs.get("type", [])
member_type = kwargs.get("member_type", str)
if typ == list and inspect.isclass(member_type) and issubclass(member_type, Enum):
values = (choice.value for choice in member_type)
return tuple(choice.value for choice in member_type)
elif inspect.isclass(typ) and issubclass(typ, Enum):
values = (choice.value for choice in typ)
return tuple(choice.value for choice in typ)
elif "choices" in kwargs:
return tuple(str(choice) for choice in kwargs["choices"])
else:
values = (str(choice) for choice in kwargs.get("choices", []))
return ", ".join(values) or None
return None

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

def get_option_scope_help_info(self, description, option_registrations_iter):
"""Returns an OptionScopeHelpInfo for the options registered with the (args, kwargs)
pairs."""
def get_option_scope_help_info(self, description: str, parser: Parser):
"""Returns an OptionScopeHelpInfo for the options parsed by the given parser."""

basic_options = []
advanced_options = []
deprecated_options = []
# Sort the arguments, so we display the help in alphabetical order.
for args, kwargs in sorted(option_registrations_iter):
for args, kwargs in sorted(parser.option_registrations_iter()):
if kwargs.get("passive"):
continue
history = parser.history(kwargs["dest"])
ohi = self.get_option_help_info(args, kwargs)
ohi = dataclasses.replace(ohi, value_history=history)
if kwargs.get("removal_version"):
deprecated_options.append(ohi)
elif kwargs.get("advanced") or (
Expand Down Expand Up @@ -293,5 +300,6 @@ def get_option_help_info(self, args, kwargs):
removal_version=removal_version,
removal_hint=removal_hint,
choices=choices,
value_history=None,
)
return ret