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

Make help-all dump a JSON blob containing all help info. #10336

Merged
merged 13 commits into from Jul 15, 2020
32 changes: 12 additions & 20 deletions 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


Expand All @@ -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
45 changes: 32 additions & 13 deletions 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
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -95,22 +101,19 @@ 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.

Assumes that self._help_request is an instance of OptionsHelp.

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)
Expand All @@ -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."
)
Expand All @@ -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."
Expand All @@ -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.
"""
Expand All @@ -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
)
23 changes: 13 additions & 10 deletions src/python/pants/option/arg_splitter.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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",
Expand Down Expand Up @@ -130,17 +134,16 @@ 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:
self._help_request = OptionsHelp()
# 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:
Expand Down
16 changes: 2 additions & 14 deletions src/python/pants/option/arg_splitter_test.py
Expand Up @@ -8,6 +8,7 @@
from typing import Dict, List, Optional

from pants.option.arg_splitter import (
AllHelp,
ArgSplitter,
NoGoalHelp,
OptionsHelp,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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=[],
Expand Down