Skip to content

Make RichHelpFormatter itself renderable with rich #90

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

Merged
merged 1 commit into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features
- Make `RichHelpFormatter` itself a rich renderable.
* PR #90

## 1.3.0 - 2023-08-19

### Features
Expand Down
94 changes: 73 additions & 21 deletions rich_argparse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(
@property
def console(self) -> r.Console: # deprecate?
if self._console is None:
self._console = r.Console(theme=r.Theme(self.styles))
self._console = r.Console(theme=r.Theme(self.styles), width=self._width)
return self._console

@console.setter
Expand All @@ -115,40 +115,93 @@ def __init__(
if parent is not None:
parent.rich_items.append(self)

def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
# empty section
if not self.rich_items and not self.rich_actions:
return
# root section
if self is self.formatter._root_section:
yield from self.rich_items
return
# group section
def _render_items(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
generated_options = options.update(no_wrap=True, overflow="ignore")
new_line = r.Segment.line()
for item in self.rich_items:
if isinstance(item, r.Padding): # user added rich renderable
item_options = options.update(width=options.max_width - item.left)
lines = r.Segment.split_lines(console.render(item.renderable, item_options))
pad = r.Segment(" " * item.left)
for line_segments in lines:
yield pad
yield from line_segments
yield new_line
else:
yield new_line
else: # argparse generated rich renderable
yield from console.render(item, generated_options)

def _render_actions(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
options = options.update(no_wrap=True, overflow="ignore")
help_pos = min(self.formatter._action_max_length + 2, self.formatter._max_help_position)
help_width = max(self.formatter._width - help_pos, 11)
if self.heading:
yield r.Text(self.heading, style="argparse.groups")
yield from self.rich_items # (optional) group description
indent = r.Text(" " * help_pos)
for action_header, action_help in self.rich_actions:
if not action_help:
yield action_header # no help, yield the header and finish
# no help, yield the header and finish
yield from console.render(action_header, options)
continue
action_help_lines = self.formatter._rich_split_lines(action_help, help_width)
if len(action_header) > help_pos - 2:
yield action_header # the header is too long, put it on its own line
# the header is too long, put it on its own line
yield from console.render(action_header, options)
action_header = indent
action_header.set_length(help_pos)
action_help_lines[0].rstrip()
yield action_header + action_help_lines[0]
yield from console.render(action_header + action_help_lines[0], options)
for line in action_help_lines[1:]:
line.rstrip()
yield indent + line
yield "\n"
yield from console.render(indent + line, options)
yield ""

def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
# empty section
if not self.rich_items and not self.rich_actions:
return
# root section
if self is self.formatter._root_section:
yield from self._render_items(console, options)
return
# group section
if self.heading:
yield r.Text(self.heading, style="argparse.groups")
if self.rich_items:
yield from self._render_items(console, options)
if self.rich_actions:
yield ""
yield from self._render_actions(console, options)

def __rich_console__(self, console: r.Console, options: r.ConsoleOptions) -> r.RenderResult:
root_renderable = console.render(self._root_section, options)
new_line = r.Segment.line()
add_empty_line = False
for line_segments in r.Segment.split_lines(root_renderable):
if len(line_segments) > 1 or (line_segments and line_segments[0]):
if add_empty_line:
yield new_line
add_empty_line = False
for i, segment in enumerate(reversed(line_segments), start=1):
stripped = segment.text.rstrip()
if stripped:
yield from line_segments[:-i]
yield r.Segment(stripped, style=segment.style, control=segment.control)
break
yield new_line
else: # empty line
add_empty_line = True

def add_text(self, text: str | None) -> None:
if text is not argparse.SUPPRESS and text is not None:
if text is argparse.SUPPRESS or text is None:
return
elif isinstance(text, str):
self._current_section.rich_items.append(self._rich_format_text(text))
else:
self.add_renderable(text)

def add_renderable(self, renderable: r.RenderableType) -> None:
padded = r.Padding.indent(renderable, self._current_indent)
self._current_section.rich_items.append(padded)

def add_usage(
self,
Expand Down Expand Up @@ -199,10 +252,9 @@ def add_argument(self, action: argparse.Action) -> None:

def format_help(self) -> str:
with self.console.capture() as capture:
self.console.print(self._root_section, highlight=False, soft_wrap=True)
self.console.print(self, highlight=False, crop=False)
help = capture.get()
if help:
help = self._long_break_matcher.sub("\n\n", help).rstrip() + "\n"
help = _fix_legacy_win_text(self.console, help)
return help

Expand Down
8 changes: 8 additions & 0 deletions rich_argparse/_lazy_rich.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"Lines",
"strip_control_codes",
"escape",
"Padding",
"Segment",
"StyleType",
"Span",
"Text",
Expand All @@ -27,6 +29,8 @@
from rich.containers import Lines as Lines
from rich.control import strip_control_codes as strip_control_codes
from rich.markup import escape as escape
from rich.padding import Padding as Padding
from rich.segment import Segment as Segment
from rich.style import StyleType as StyleType
from rich.text import Span as Span
from rich.text import Text as Text
Expand All @@ -41,6 +45,8 @@ def __getattr__(name: str) -> Any:
import rich.containers
import rich.control
import rich.markup
import rich.padding
import rich.segment
import rich.style
import rich.text
import rich.theme
Expand All @@ -55,6 +61,8 @@ def __getattr__(name: str) -> Any:
"Lines": rich.containers.Lines,
"strip_control_codes": rich.control.strip_control_codes,
"escape": rich.markup.escape,
"Padding": rich.padding.Padding,
"Segment": rich.segment.Segment,
"StyleType": rich.style.StyleType,
"Span": rich.text.Span,
"Text": rich.text.Text,
Expand Down
53 changes: 52 additions & 1 deletion tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

import pytest
from rich import get_console
from rich.console import Group
from rich.markdown import Markdown
from rich.table import Table
from rich.text import Text

import rich_argparse._lazy_rich as r
Expand Down Expand Up @@ -357,7 +360,7 @@ def test_generated_usage():
(
"PROG "
"\x1b[38;5;244mPROG\x1b[0m "
"\x1b[1m \x1b[0m\x1b[1;38;5;244mPROG\x1b[0m\x1b[1m \x1b[0m"
"\x1b[1m \x1b[0m\x1b[1;38;5;244mPROG\x1b[0m" # "\x1b[1m \x1b[0m"
"\n\x1b[38;5;244m'PROG'\x1b[0m"
),
True,
Expand Down Expand Up @@ -859,3 +862,51 @@ def test_no_win_console_init_on_unix(): # pragma: win32 no cover
out = _fix_legacy_win_text(console, text)
assert out == text
init_win_colors.assert_not_called()


@pytest.mark.usefixtures("force_color")
def test_rich_renderables():
table = Table("foo", "bar")
table.add_row("1", "2")
parser = ArgumentParser(
"PROG",
formatter_class=RichHelpFormatter,
description=Markdown(
textwrap.dedent(
"""\
This is a **description**
_________________________

| foo | bar |
| --- | --- |
| 1 | 2 |
"""
)
),
epilog=Group(Markdown("This is an *epilog*"), table, Text("The end.", style="red")),
)
expected_help = """\
\x1b[38;5;208mUsage:\x1b[0m \x1b[38;5;244mPROG\x1b[0m [\x1b[36m-h\x1b[0m]

This is a \x1b[1mdescription\x1b[0m

\x1b[33m──────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m


\x1b[1m \x1b[0m\x1b[1mfoo\x1b[0m\x1b[1m \x1b[0m \x1b[1m \x1b[0m\x1b[1mbar\x1b[0m
━━━━━━━━━━━
1 2


\x1b[38;5;208mOptional Arguments:\x1b[0m
\x1b[36m-h\x1b[0m, \x1b[36m--help\x1b[0m \x1b[39mshow this help message and exit\x1b[0m

This is an \x1b[3mepilog\x1b[0m
┏━━━━━┳━━━━━┓
┃\x1b[1m \x1b[0m\x1b[1mfoo\x1b[0m\x1b[1m \x1b[0m┃\x1b[1m \x1b[0m\x1b[1mbar\x1b[0m\x1b[1m \x1b[0m┃
┡━━━━━╇━━━━━┩
│ 1 │ 2 │
└─────┴─────┘
\x1b[31mThe end.\x1b[0m
"""
assert parser.format_help() == clean(expected_help)