diff --git a/src/python/pants/help/help_integration_test.py b/src/python/pants/help/help_integration_test.py index 28477039955..76b5c7dcc0f 100644 --- a/src/python/pants/help/help_integration_test.py +++ b/src/python/pants/help/help_integration_test.py @@ -1,6 +1,8 @@ # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import json + from pants.testutil.pants_run_integration_test import PantsRunIntegrationTest @@ -26,23 +28,13 @@ def test_help_all(self): command = ["help-all"] pants_run = self.run_pants(command=command) self.assert_success(pants_run) - # Spot check to see that scope headings are printed - assert "`pytest` subsystem options" in pants_run.stdout_data - # Spot check to see that full args for all options are printed - assert "--[no-]test-debug" in pants_run.stdout_data - # Spot check to see that subsystem options are printing - assert "--pytest-version" in pants_run.stdout_data - - def test_help_all_advanced(self): - command = ["--help-all", "--help-advanced"] - pants_run = self.run_pants(command=command) - self.assert_success(pants_run) - # Spot check to see that scope headings are printed even for advanced options - assert "`pytest` subsystem options" in pants_run.stdout_data - assert "`pytest` subsystem advanced options" in pants_run.stdout_data - # Spot check to see that full args for all options are printed - assert "--[no-]test-debug" in pants_run.stdout_data - # Spot check to see that subsystem options are printing - assert "--pytest-version" in pants_run.stdout_data - # Spot check to see that advanced subsystem options are printing - assert "--pytest-timeout-default" in pants_run.stdout_data + all_help = json.loads(pants_run.stdout_data) + + # Spot check the data. + assert "name_to_goal_info" in all_help + assert "test" in all_help["name_to_goal_info"] + + assert "scope_to_help_info" in all_help + assert "" in all_help["scope_to_help_info"] + assert "pytest" in all_help["scope_to_help_info"] + assert len(all_help["scope_to_help_info"]["pytest"]["basic"]) > 0 diff --git a/src/python/pants/help/help_printer.py b/src/python/pants/help/help_printer.py index 3cc90e8f18c..3dd670d0797 100644 --- a/src/python/pants/help/help_printer.py +++ b/src/python/pants/help/help_printer.py @@ -1,8 +1,11 @@ # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import dataclasses +import json import sys import textwrap +from enum import Enum from typing import Dict, cast from colors import cyan, green @@ -12,6 +15,7 @@ from pants.help.help_formatter import HelpFormatter from pants.help.help_info_extracter import AllHelpInfo from pants.option.arg_splitter import ( + AllHelp, GoalsHelp, HelpRequest, NoGoalHelp, @@ -42,10 +46,12 @@ def print_hint() -> None: if isinstance(self._help_request, VersionHelp): print(pants_version()) - elif isinstance(self._help_request, OptionsHelp): - self._print_options_help() elif isinstance(self._help_request, GoalsHelp): self._print_goals_help() + elif isinstance(self._help_request, AllHelp): + self._print_all_help() + elif isinstance(self._help_request, OptionsHelp): + self._print_options_help() elif isinstance(self._help_request, UnknownGoalHelp): print("Unknown goals: {}".format(", ".join(self._help_request.unknown_goals))) print_hint() @@ -95,6 +101,9 @@ def format_goal(name: str, descr: str) -> str: ] print("\n".join(lines)) + def _print_all_help(self) -> None: + print(self._get_help_json()) + def _print_options_help(self) -> None: """Print a help screen. @@ -102,15 +111,9 @@ def _print_options_help(self) -> None: Note: Ony useful if called after options have been registered. """ - help_request = cast(OptionsHelp, self._help_request) - - if help_request.all_scopes: - help_scopes = set(self._all_help_info.scope_to_help_info.keys()) - else: - # The scopes explicitly mentioned by the user on the cmd line. - help_scopes = set(help_request.scopes) - + # The scopes explicitly mentioned by the user on the cmd line. + help_scopes = set(help_request.scopes) if help_scopes: for scope in sorted(help_scopes): help_str = self._format_help(scope, help_request.advanced) @@ -126,7 +129,7 @@ def _print_global_help(self, advanced: bool): print( f" {self._bin_name} [option ...] [goal ...] [target/file ...] Attempt the specified goals." ) - print(f" {self._bin_name} help Get help.") + print(f" {self._bin_name} help Get global help.") print( f" {self._bin_name} help [goal/subsystem] Get help for a goal or subsystem." ) @@ -137,7 +140,7 @@ def _print_global_help(self, advanced: bool): f" {self._bin_name} help-advanced [goal/subsystem] Get help for a goal's or subsystem's advanced options." ) print( - f" {self._bin_name} help-all Get help for all goals and subsystems." + f" {self._bin_name} help-all Get a JSON object containing all help info." ) print( f" {self._bin_name} goals List all installed goals." @@ -154,7 +157,7 @@ def _print_global_help(self, advanced: bool): print(self._format_help(GLOBAL_SCOPE, advanced)) def _format_help(self, scope: str, show_advanced_and_deprecated: bool) -> str: - """Return a help message for the options registered on this object. + """Return a human-readable help message for the options registered on this object. Assumes that self._help_request is an instance of OptionsHelp. """ @@ -177,3 +180,19 @@ def _format_help(self, scope: str, show_advanced_and_deprecated: bool) -> str: formatted_lines.append(f"{related_subsystems_label} {', '.join(related_scopes)}") formatted_lines.append("") return "\n".join(formatted_lines) + + class Encoder(json.JSONEncoder): + def default(self, o): + if callable(o): + return o.__name__ + if isinstance(o, type): + return type.__name__ + if isinstance(o, Enum): + return o.value + return super().default(o) + + def _get_help_json(self) -> str: + """Return a JSON object containing all the help info we have, for every scope.""" + return json.dumps( + dataclasses.asdict(self._all_help_info), sort_keys=True, indent=2, cls=self.Encoder + ) diff --git a/src/python/pants/option/arg_splitter.py b/src/python/pants/option/arg_splitter.py index b65c269683a..116cf15ad02 100644 --- a/src/python/pants/option/arg_splitter.py +++ b/src/python/pants/option/arg_splitter.py @@ -36,16 +36,19 @@ class OptionsHelp(HelpRequest): """The user requested help on which options they can set.""" advanced: bool = False - all_scopes: bool = False scopes: Tuple[str, ...] = () +class VersionHelp(HelpRequest): + """The user asked for the version of this instance of pants.""" + + class GoalsHelp(HelpRequest): """The user requested help for installed Goals.""" -class VersionHelp(HelpRequest): - """The user asked for the version of this instance of pants.""" +class AllHelp(HelpRequest): + """The user requested a dump of all help info.""" @dataclass(frozen=True) @@ -74,15 +77,15 @@ class ArgSplitter: _HELP_BASIC_ARGS = ("-h", "--help", "help") _HELP_ADVANCED_ARGS = ("--help-advanced", "help-advanced") - _HELP_ALL_SCOPES_ARGS = ("--help-all", "help-all") - _HELP_VERSION_ARGS = ("-v", "-V", "--version") + _HELP_VERSION_ARGS = ("-v", "-V", "--version", "version") _HELP_GOALS_ARGS = ("goals",) + _HELP_ALL_SCOPES_ARGS = ("help-all",) _HELP_ARGS = ( *_HELP_BASIC_ARGS, *_HELP_ADVANCED_ARGS, - *_HELP_ALL_SCOPES_ARGS, *_HELP_VERSION_ARGS, *_HELP_GOALS_ARGS, + *_HELP_ALL_SCOPES_ARGS, ) def __init__(self, known_scope_infos: Iterable[ScopeInfo]) -> None: @@ -91,6 +94,7 @@ def __init__(self, known_scope_infos: Iterable[ScopeInfo]) -> None: # that we heuristically identify target specs based on it containing /, : or being # a top-level directory. self._known_scopes = {si.scope for si in known_scope_infos} | { + "version", "goals", "help", "help-advanced", @@ -130,6 +134,8 @@ def _check_for_help_request(self, arg: str) -> bool: self._help_request = VersionHelp() elif arg in self._HELP_GOALS_ARGS: self._help_request = GoalsHelp() + elif arg in self._HELP_ALL_SCOPES_ARGS: + self._help_request = AllHelp() else: # First ensure that we have a basic OptionsHelp. if not self._help_request: @@ -137,10 +143,7 @@ def _check_for_help_request(self, arg: str) -> bool: # Now see if we need to enhance it. if isinstance(self._help_request, OptionsHelp): advanced = self._help_request.advanced or arg in self._HELP_ADVANCED_ARGS - all_scopes = self._help_request.all_scopes or arg in self._HELP_ALL_SCOPES_ARGS - self._help_request = dataclasses.replace( - self._help_request, advanced=advanced, all_scopes=all_scopes - ) + self._help_request = dataclasses.replace(self._help_request, advanced=advanced) return True def split_args(self, args: Sequence[str]) -> SplitArgs: diff --git a/src/python/pants/option/arg_splitter_test.py b/src/python/pants/option/arg_splitter_test.py index 95f2c70b1d2..1591d0944e4 100644 --- a/src/python/pants/option/arg_splitter_test.py +++ b/src/python/pants/option/arg_splitter_test.py @@ -8,6 +8,7 @@ from typing import Dict, List, Optional from pants.option.arg_splitter import ( + AllHelp, ArgSplitter, NoGoalHelp, OptionsHelp, @@ -56,9 +57,7 @@ def assert_valid_split( assert expected_help_advanced == ( isinstance(splitter.help_request, OptionsHelp) and splitter.help_request.advanced ) - assert expected_help_all == ( - isinstance(splitter.help_request, OptionsHelp) and splitter.help_request.all_scopes - ) + assert expected_help_all == isinstance(splitter.help_request, AllHelp) assert expected_unknown_scopes == split_args.unknown_scopes @staticmethod @@ -299,17 +298,6 @@ def test_help_detection(self) -> None: assert_help_no_arguments("./pants --help --help-advanced", expected_help_advanced=True) assert_help_no_arguments("./pants --help-advanced --help", expected_help_advanced=True) assert_help_no_arguments("./pants help-all", expected_help_all=True) - assert_help_no_arguments("./pants --help-all", expected_help_all=True) - assert_help_no_arguments("./pants --help --help-all", expected_help_all=True) - assert_help_no_arguments( - "./pants help-advanced --help-all", expected_help_advanced=True, expected_help_all=True, - ) - assert_help_no_arguments( - "./pants --help-all --help --help-advanced", - expected_help_advanced=True, - expected_help_all=True, - ) - assert_help( "./pants -f", expected_goals=[],