From dfddfd659ca934f18e656b3dba181e779dd52c5a Mon Sep 17 00:00:00 2001 From: Benjy Weinberger Date: Wed, 15 Jul 2020 12:11:04 -0700 Subject: [PATCH] Display multiline help nicely. (#10366) This includes: - Respecting any embedded newlines in the option help string. - Formatting option values (defaults, current value) nicely when they are large lists and dicts. [ci skip-rust-tests] --- .../backend/python/rules/download_pex_bin.py | 2 +- .../pants/core/util_rules/external_tool.py | 31 +++++++++++------ src/python/pants/help/help_formatter.py | 34 +++++++++++++------ src/python/pants/option/parser.py | 2 +- 4 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/python/pants/backend/python/rules/download_pex_bin.py b/src/python/pants/backend/python/rules/download_pex_bin.py index 174fdbe240d..2b65dbe2928 100644 --- a/src/python/pants/backend/python/rules/download_pex_bin.py +++ b/src/python/pants/backend/python/rules/download_pex_bin.py @@ -28,7 +28,7 @@ class PexBin(ExternalTool): default_version = "v2.1.13" default_known_versions = [ f"v2.1.13|{plat}|240712c75bb7c7cdfe3dd808ad6e6f186182e6aea3efeea5760683bb0fe89198|2633838" - for plat in ["darwin", "linux"] + for plat in ["darwin", "linux "] ] def generate_url(self, plat: Platform) -> str: diff --git a/src/python/pants/core/util_rules/external_tool.py b/src/python/pants/core/util_rules/external_tool.py index 29e0ad0d711..85f93574bdc 100644 --- a/src/python/pants/core/util_rules/external_tool.py +++ b/src/python/pants/core/util_rules/external_tool.py @@ -1,6 +1,7 @@ # Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import textwrap from abc import abstractmethod from dataclasses import dataclass from typing import List @@ -97,22 +98,32 @@ def register_options(cls, register): fingerprint=True, help=f"Use this version of {cls.name}.", ) + + help_str = textwrap.dedent( + f""" + Known versions to verify downloads against. + Each element is a pipe-separated string of `version|platform|sha256|length`, where: + + - `version` is the version string + - `platform` is one of [{','.join(Platform.__members__.keys())}], + - `sha256` is the 64-character hex representation of the expected sha256 + digest of the download file, as emitted by `shasum -a 256` + - `length` is the expected length of the download file in bytes + + E.g., '3.1.2|darwin|6d0f18cd84b918c7b3edd0203e75569e0c7caecb1367bbbe409b44e28514f5be|42813'. + + Values are space-stripped, so pipes can be indented for readability if necessary. + You can compute the length and sha256 easily with: + `curl -L $URL | tee >(wc -c) >(shasum -a 256) >/dev/null` + """ + ) register( "--known-versions", type=list, member_type=str, default=cls.default_known_versions, advanced=True, - help=f"Known versions to verify downloads against. Each element is a " - f"pipe-separated string of version|platform|sha256|length, where `version` is the " - f"version string, `platform` is one of [{','.join(Platform.__members__.keys())}], " - f"`sha256` is the 64-character hex representation of the expected sha256 digest of the " - f"download file, as emitted by `shasum -a 256`, and `length` is the expected length of " - f"the download file in bytes. E.g., '3.1.2|darwin|" - f"6d0f18cd84b918c7b3edd0203e75569e0c7caecb1367bbbe409b44e28514f5be|42813'. " - f"Values are space-stripped, so pipes can be indented for readability if necessary." - f"You can compute the length and sha256 easily with: " - f"curl -L $URL | tee >(wc -c) >(shasum -a 256) >/dev/null", + help=help_str, ) @abstractmethod diff --git a/src/python/pants/help/help_formatter.py b/src/python/pants/help/help_formatter.py index bb0ed8279a0..4142e0d4d4d 100644 --- a/src/python/pants/help/help_formatter.py +++ b/src/python/pants/help/help_formatter.py @@ -1,13 +1,14 @@ # Copyright 2015 Pants project contributors (see CONTRIBUTORS.md). # Licensed under the Apache License, Version 2.0 (see LICENSE). +import json from textwrap import wrap 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 +from pants.option.ranked_value import Rank, RankedValue class HelpFormatter: @@ -71,21 +72,29 @@ def format_option(self, ohi: OptionHelpInfo) -> List[str]: def maybe_parens(s: Optional[str]) -> str: return f" ({s})" if s else "" + def format_value(val: RankedValue, prefix: str, left_padding: str) -> List[str]: + if isinstance(val.value, (list, dict)): + val_lines = json.dumps(val.value, sort_keys=True, indent=4).split("\n") + else: + val_lines = [f"{val.value}"] + val_lines[0] = f"{prefix}{val_lines[0]}" + val_lines[-1] = f"{val_lines[-1]}{maybe_parens(val.details)}" + val_lines = [self._maybe_cyan(f"{left_padding}{line}") for line in val_lines] + return val_lines + indent = " " arg_lines = [f" {self._maybe_magenta(args)}" for args in ohi.display_args] 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}", 80)) + for i, s in enumerate(wrap(f"{choices}", 96)) ] - default_line = self._maybe_cyan(f"{indent}default: {ohi.default}") + default_lines = format_value(RankedValue(Rank.HARDCODED, ohi.default), "default: ", indent) 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)}" - ) + curr_value_lines = format_value(final_val, "current value: ", indent) interesting_ranked_values = [ rv @@ -93,15 +102,20 @@ def maybe_parens(s: Optional[str]) -> str: 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)}") + line for rv in interesting_ranked_values + for line in format_value(rv, "overrode: ", f"{indent} ") + ] + description_lines = ohi.help.splitlines() + # wrap() returns [] for an empty line, but we want to emit those, hence the "or [line]". + description_lines = [ + f"{indent}{s}" for line in description_lines for s in wrap(line, 96) or [line] ] - description_lines = [f"{indent}{s}" for s in wrap(ohi.help, 80)] lines = [ *arg_lines, *choices_lines, - default_line, - curr_value_line, + *default_lines, + *curr_value_lines, *value_derivation_lines, *description_lines, ] diff --git a/src/python/pants/option/parser.py b/src/python/pants/option/parser.py index 14c93a79191..f466cdacb59 100644 --- a/src/python/pants/option/parser.py +++ b/src/python/pants/option/parser.py @@ -77,7 +77,7 @@ class OptionValueHistory: ranked_values: Tuple[RankedValue] @property - def final_value(self): + def final_value(self) -> RankedValue: return self.ranked_values[-1]