Skip to content

Commit

Permalink
Improve formatting for long helptext
Browse files Browse the repository at this point in the history
  • Loading branch information
brentyi committed Aug 26, 2022
1 parent 3c3167f commit e4da3fc
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 18 deletions.
88 changes: 82 additions & 6 deletions dcargs/_argparse_formatter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import argparse
import contextlib
import itertools
import re as _re
from gettext import gettext as _
from gettext import ngettext
from typing import Any, ContextManager, Generator

from . import _strings


def monkeypatch_len(obj: Any) -> int:
if isinstance(obj, str):
return len(_strings.strip_ansi_sequences(obj))
else:
return len(obj)


def ansi_context() -> ContextManager[None]:
"""Context for working with ANSI codes + argparse:
- Applies a temporary monkey patch for making argparse ignore ANSI codes when
Expand All @@ -14,14 +25,9 @@ def ansi_context() -> ContextManager[None]:

@contextlib.contextmanager
def inner() -> Generator[None, None, None]:
def monkeypatched_len(obj: Any) -> int:
if isinstance(obj, str):
return len(_strings.strip_ansi_sequences(obj))
else:
return len(obj)

if not hasattr(argparse, "len"):
argparse.len = monkeypatched_len # type: ignore
argparse.len = monkeypatch_len # type: ignore
try:
# Use Colorama to support coloring in Windows shells.
import colorama # type: ignore
Expand Down Expand Up @@ -54,8 +60,78 @@ def monkeypatched_len(obj: Any) -> int:


class ArgparseHelpFormatter(argparse.RawDescriptionHelpFormatter):
def __init__(
self,
prog,
indent_increment=2,
max_help_position=64, # Usually 24
width=None,
):
super().__init__(prog, indent_increment, max_help_position, width)

def _format_args(self, action, default_metavar):
"""Override _format_args() to ignore nargs and always expect single string
metavars."""
get_metavar = self._metavar_formatter(action, default_metavar)
return get_metavar(1)[0]

def _format_action(self, action):
# determine the required width and the entry label
help_position = min(self._action_max_length + 2, self._max_help_position)
help_width = max(self._width - help_position, 11)
action_width = help_position - self._current_indent - 2
action_header = self._format_action_invocation(action)

# no help; start on same line and add a final newline
if not action.help:
tup = self._current_indent, "", action_header
action_header = "%*s%s\n" % tup

# short action name; start on the same line and pad two spaces
elif monkeypatch_len(action_header) <= action_width:
# Original:
# tup = self._current_indent, "", action_width, action_header
# action_header = "%*s%-*s " % tup
# <new>
action_header = (
" " * self._current_indent
+ action_header
+ " " * (action_width - monkeypatch_len(action_header))
)
# </new>
indent_first = 0

# long action name; start on the next line
else:
tup = self._current_indent, "", action_header
action_header = "%*s%s\n" % tup
indent_first = help_position

# collect the pieces of the action help
parts = [action_header]

# if there was help for the action, add lines of help text
if action.help:
help_text = self._expand_help(action)
# <new>
# Respect existing line breaks.
help_lines = tuple(
itertools.chain(
*(self._split_lines(h, help_width) for h in help_text.split("\n"))
)
)
# </new>
parts.append("%*s%s\n" % (indent_first, "", help_lines[0]))
for line in help_lines[1:]:
parts.append("%*s%s\n" % (help_position, "", line))

# or add a newline if the description doesn't end with one
elif not action_header.endswith("\n"):
parts.append("\n")

# if there are any sub-actions, add their help as well
for subaction in self._iter_indented_subactions(action):
parts.append(self._format_action(subaction))

# return a single string
return self._join_parts(parts)
18 changes: 8 additions & 10 deletions dcargs/_instantiators.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
import termcolor
from typing_extensions import Annotated, Final, Literal, get_args, get_origin

from . import _strings

_StandardInstantiator = Callable[[List[str]], Any]
# Special case: the only time that argparse doesn't give us a string is when the
# argument action is set to `store_true` or `store_false`. In this case, we get
Expand Down Expand Up @@ -94,10 +96,6 @@ class UnsupportedTypeAnnotationError(Exception):
)


def _format_metavar(x: str) -> str:
return termcolor.colored(x, attrs=["bold"])


def instantiator_from_type(
typ: Type, type_from_typevar: Dict[TypeVar, Type]
) -> Tuple[Instantiator, InstantiatorMetadata]:
Expand Down Expand Up @@ -131,7 +129,7 @@ def instantiator(strings: List[str]) -> None:

return instantiator, InstantiatorMetadata(
nargs=1,
metavar="{" + _format_metavar("None") + "}",
metavar="{" + _strings.format_metavar("None") + "}",
choices=("None",),
)

Expand Down Expand Up @@ -218,9 +216,9 @@ def instantiator_base_case(strings: List[str]) -> Any:

return instantiator_base_case, InstantiatorMetadata(
nargs=1,
metavar=_format_metavar(typ.__name__.upper())
metavar=_strings.format_metavar(typ.__name__.upper())
if auto_choices is None
else "{" + ",".join(map(_format_metavar, map(str, auto_choices))) + "}",
else "{" + ",".join(map(_strings.format_metavar, map(str, auto_choices))) + "}",
choices=auto_choices,
)

Expand Down Expand Up @@ -519,7 +517,7 @@ def dict_instantiator(strings: List[str]) -> Any:
pair_metavar = f"{key_meta.metavar} {val_meta.metavar}"
return dict_instantiator, InstantiatorMetadata(
nargs="+",
metavar=f"{pair_metavar} [{pair_metavar} ...]",
metavar=_strings.multi_metavar_from_single(pair_metavar),
choices=None,
)

Expand Down Expand Up @@ -563,7 +561,7 @@ def sequence_instantiator(strings: List[str]) -> Any:

return sequence_instantiator, InstantiatorMetadata(
nargs="+",
metavar=f"{inner_meta.metavar} [{inner_meta.metavar} ...]",
metavar=_strings.multi_metavar_from_single(inner_meta.metavar),
choices=inner_meta.choices,
)

Expand All @@ -579,7 +577,7 @@ def _instantiator_from_literal(
lambda strings: choices[str_choices.index(strings[0])],
InstantiatorMetadata(
nargs=1,
metavar="{" + ",".join(map(_format_metavar, str_choices)) + "}",
metavar="{" + ",".join(map(_strings.format_metavar, str_choices)) + "}",
choices=str_choices,
),
)
14 changes: 14 additions & 0 deletions dcargs/_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import textwrap
from typing import Iterable, List, Sequence, Type, Union

import termcolor

from . import _resolver

dummy_field_name = "__dcargs_dummy_field_name__"
Expand Down Expand Up @@ -80,3 +82,15 @@ def _get_ansi_pattern() -> re.Pattern:

def strip_ansi_sequences(x: str):
return _get_ansi_pattern().sub("", x)


def format_metavar(x: str) -> str:
return termcolor.colored(x, attrs=["bold"])


def multi_metavar_from_single(single: str) -> str:
if len(strip_ansi_sequences(single)) >= 32:
# Shorten long metavars
return f"{single} [...]"
else:
return f"{single} [{single} ...]"
6 changes: 4 additions & 2 deletions tests/test_dict_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@ class HelptextNamedTupleDefault(NamedTuple):
dcargs.cli(HelptextNamedTupleDefault, args=["--help"])
helptext = dcargs._strings.strip_ansi_sequences(f.getvalue())
assert cast(str, HelptextNamedTupleDefault.__doc__) in helptext
assert "--x INT Documentation 1 (required)\n" in helptext
assert "--y INT Documentation 2 (required)\n" in helptext
assert "--x INT" in helptext
assert "--y INT" in helptext
assert "--z INT" in helptext
assert "Documentation 1 (required)\n" in helptext
assert "Documentation 2 (required)\n" in helptext
assert "Documentation 3 (default: 3)\n" in helptext


Expand Down

0 comments on commit e4da3fc

Please sign in to comment.