From 17011e333803716f6dea2fac3413b74f0bfb5794 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 6 Jun 2020 15:34:59 +0100 Subject: [PATCH] table tests, optimizations --- CHANGELOG.md | 4 ++- rich/_pick.py | 17 +++++++++++++ rich/console.py | 2 +- rich/text.py | 57 +++++++++++++++++++------------------------ tests/test_console.py | 5 ++-- tests/test_table.py | 34 ++++++++++++++++++++++++++ 6 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 rich/_pick.py create mode 100644 tests/test_table.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7cf1ba..5a697282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added overflow methods - Added no_wrap option to print() - Added width option to print +- Improved handling of compressed tables ### Fixed -- Improved handling of compressed tables +- Fixed erroneous space at end of log ### Changed - Renamed \_ratio.ratio_divide to \_ratio.ratio_distribute - Renamed JustifyValues to JustifyMethod +- Optimized \_trim_spans ## [1.3.1] - 2020-06-01 diff --git a/rich/_pick.py b/rich/_pick.py new file mode 100644 index 00000000..169b1cf5 --- /dev/null +++ b/rich/_pick.py @@ -0,0 +1,17 @@ +from typing import Optional + + +def pick_bool(*values: Optional[bool]) -> bool: + """Pick the first non-none bool or return the last value. + + Args: + *values (bool): Any number of boolean or None values. + + Returns: + bool: First non-none boolean. + """ + assert values, "1 or more values required" + for value in values: + if value is not None: + return value + return bool(value) diff --git a/rich/console.py b/rich/console.py index 1ef6660c..cb24f2b0 100644 --- a/rich/console.py +++ b/rich/console.py @@ -93,7 +93,7 @@ class ConsoleOptions: encoding: str justify: Optional[JustifyMethod] = None overflow: Optional[OverflowMethod] = None - no_wrap: bool = False + no_wrap: Optional[bool] = False def update( self, diff --git a/rich/text.py b/rich/text.py index aa42996b..ce3839d8 100644 --- a/rich/text.py +++ b/rich/text.py @@ -23,6 +23,7 @@ from .containers import Lines from .control import strip_control_codes from .jupyter import JupyterMixin from .measure import Measurement +from ._pick import pick_bool from .segment import Segment from .style import Style, StyleType @@ -114,7 +115,7 @@ class Text(JupyterMixin): style: Union[str, Style] = "", justify: "JustifyMethod" = None, overflow: "OverflowMethod" = None, - no_wrap: bool = False, + no_wrap: bool = None, end: str = "\n", tab_size: Optional[int] = 8, spans: List[Span] = None, @@ -423,28 +424,16 @@ class Text(JupyterMixin): "OverflowMethod", self.overflow or options.overflow or DEFAULT_OVERFLOW ) - if self.no_wrap or options.no_wrap: - render_text = self - if overflow in ("crop", "ellipsis"): - render_text = self.copy() - render_text.truncate(options.max_width, overflow) - if justify: - lines = Lines([render_text]) - lines.justify( - console, options.max_width, justify=justify, overflow=overflow - ) - render_text = lines[0] - yield from render_text.render(console, end=self.end) - else: - lines = self.wrap( - console, - options.max_width, - justify=justify, - overflow=overflow, - tab_size=tab_size or 8, - ) - all_lines = Text("\n").join(lines) - yield from all_lines.render(console, end=self.end) + lines = self.wrap( + console, + options.max_width, + justify=justify, + overflow=overflow, + tab_size=tab_size or 8, + no_wrap=pick_bool(options.no_wrap, self.no_wrap, False), + ) + all_lines = Text("\n").join(lines) + yield from all_lines.render(console, end=self.end) def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: text = self.plain @@ -574,7 +563,7 @@ class Text(JupyterMixin): Args: max_width (int): Maximum number of characters in text. - overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None, to use self.overflow. + overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow. pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False. """ length = cell_len(self.plain) @@ -584,7 +573,6 @@ class Text(JupyterMixin): self.plain = set_cell_size(self.plain, max_width - 1).rstrip() + "…" else: self.plain = set_cell_size(self.plain, max_width) - length = cell_len(self.plain) if pad and length < max_width: spaces = max_width - length self.plain = f"{self.plain}{' ' * spaces}" @@ -785,6 +773,7 @@ class Text(JupyterMixin): justify: "JustifyMethod" = None, overflow: "OverflowMethod" = None, tab_size: int = 8, + no_wrap: bool = None, ) -> Lines: """Word wrap the text. @@ -792,9 +781,10 @@ class Text(JupyterMixin): console (Console): Console instance. width (int): Number of characters per line. emoji (bool, optional): Also render emoji code. Defaults to True. - justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to "left". - overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None. + justify (str, optional): Justify method: "default", "left", "center", "full", "right". Defaults to "default". + overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None. tab_size (int, optional): Default tab size. Defaults to 8. + no_wrap (bool, optional): Disable wrapping, Defaults to False. Returns: Lines: Number of lines. @@ -803,23 +793,26 @@ class Text(JupyterMixin): wrap_overflow = cast( "OverflowMethod", overflow or self.overflow or DEFAULT_OVERFLOW ) + no_wrap = pick_bool(no_wrap, self.no_wrap, False) lines: Lines = Lines() for line in self.split(): if "\t" in line: line = line.tabs_to_spaces(tab_size) - offsets = divide_line(str(line), width, fold=wrap_overflow == "fold") - new_lines = line.divide(offsets) + if no_wrap: + new_lines = Lines([line]) + else: + offsets = divide_line(str(line), width, fold=wrap_overflow == "fold") + new_lines = line.divide(offsets) for line in new_lines: line.rstrip_end(width) if wrap_justify: new_lines.justify( console, width, justify=wrap_justify, overflow=wrap_overflow ) + for line in new_lines: + line.truncate(width, wrap_overflow) lines.extend(new_lines) - for line in lines: - line.truncate(width, wrap_overflow) - return lines def fit(self, width: int) -> Lines: diff --git a/tests/test_console.py b/tests/test_console.py index 26d6911d..89c38c17 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -248,6 +248,5 @@ def test_save_html(): def test_no_wrap(): console = Console(width=10, file=io.StringIO()) - console.print("X" * 15) - console.print("Y" * 15, no_wrap=True) - assert console.file.getvalue() == "XXXXXXXXXX\nXXXXX\nYYYYYYYYYY\n" + console.print("foo bar baz egg", no_wrap=True) + assert console.file.getvalue() == "foo bar ba\n" diff --git a/tests/test_table.py b/tests/test_table.py new file mode 100644 index 00000000..cb139572 --- /dev/null +++ b/tests/test_table.py @@ -0,0 +1,34 @@ +# encoding=utf-8 + +import io + + +from rich.console import Console +from rich.table import Table + + +def render_tables(): + console = Console(width=60, file=io.StringIO()) + + table = Table(title="test table", caption="table footer", expand=True) + table.add_column("foo", no_wrap=True, overflow="ellipsis") + table.add_column("bar", justify="center") + table.add_column("baz", justify="right") + + table.add_row("Averlongwordgoeshere", "banana pancakes", None) + + for width in range(10, 60, 5): + console.print(table, width=width) + + return console.file.getvalue() + + +def test_render_table(): + expected = "test table\n┏━━┳━━┳━━┓\n┃ ┃ ┃ ┃\n┡━━╇━━╇━━┩\n│ │ │ │\n└──┴──┴──┘\n table \n footer \n test table \n┏━━━━━┳━━━━┳━━┓\n┃ foo ┃ b… ┃ ┃\n┡━━━━━╇━━━━╇━━┩\n│ Av… │ b… │ │\n└─────┴────┴──┘\n table footer \n test table \n┏━━━━━━━━┳━━━━━┳━━━┓\n┃ foo ┃ bar ┃ … ┃\n┡━━━━━━━━╇━━━━━╇━━━┩\n│ Averl… │ ba… │ │\n└────────┴─────┴───┘\n table footer \n test table \n┏━━━━━━━━━━━━┳━━━━━━┳━━━┓\n┃ foo ┃ bar ┃ … ┃\n┡━━━━━━━━━━━━╇━━━━━━╇━━━┩\n│ Averlongw… │ ban… │ │\n└────────────┴──────┴───┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━┩\n│ Averlongword… │ bana… │ │\n└───────────────┴───────┴────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━┩\n│ Averlongwordgoe… │ banana… │ │\n└──────────────────┴─────────┴────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshe… │ banana │ │\n│ │ pancakes │ │\n└─────────────────────┴──────────┴─────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancakes │ │\n└──────────────────────┴──────────────┴─────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└───────────────────────┴─────────────────┴──────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└─────────────────────────┴────────────────────┴──────┘\n table footer \n" + assert render_tables() == expected + + +if __name__ == "__main__": + render = render_tables() + print(render) + print(repr(render))