diff --git a/CHANGELOG.md b/CHANGELOG.md index ed011e8db..b5a4c0603 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.3.0] - 2020-07-12 + +### Added + +- Added title and title_align options to Panel +- Added pad and width parameters to Align +- Added end parameter to Rule +- Added Text.pad and Text.align methods +- Added leading parameter to Table + ## [3.2.0] - 2020-07-10 ### Added diff --git a/pyproject.toml b/pyproject.toml index 074c02483..85cca92d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "rich" homepage = "https://github.com/willmcgugan/rich" documentation = "https://rich.readthedocs.io/en/latest/" -version = "3.2.0" +version = "3.3.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" diff --git a/rich/align.py b/rich/align.py index 6e31f6ab9..046323eca 100644 --- a/rich/align.py +++ b/rich/align.py @@ -1,6 +1,7 @@ -from typing import TYPE_CHECKING +from typing import Iterable, TYPE_CHECKING from typing_extensions import Literal +from .constrain import Constrain from .jupyter import JupyterMixin from .measure import Measurement from .segment import Segment @@ -20,13 +21,21 @@ class Align(JupyterMixin): renderable (RenderableType): A console renderable. align (AlignValues): One of "left", "center", or "right"" style (StyleType, optional): An optional style to apply to the renderable. + pad (bool, optional): Pad the right with spaces. Defaults to True. + width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None. Raises: ValueError: if ``align`` is not one of the expected values. """ def __init__( - self, renderable: "RenderableType", align: AlignValues, style: StyleType = None + self, + renderable: "RenderableType", + align: AlignValues, + style: StyleType = None, + *, + pad: bool = True, + width: int = None, ) -> None: if align not in ("left", "center", "right"): raise ValueError( @@ -35,35 +44,58 @@ def __init__( self.renderable = renderable self.align = align self.style = style + self.pad = pad + self.width = width @classmethod - def left(cls, renderable: "RenderableType", style: StyleType = None) -> "Align": + def left( + cls, + renderable: "RenderableType", + style: StyleType = None, + *, + pad: bool = True, + width: int = None, + ) -> "Align": """Align a renderable to the left.""" - return cls(renderable, "left", style=style) + return cls(renderable, "left", style=style, pad=pad, width=width) @classmethod - def center(cls, renderable: "RenderableType", style: StyleType = None) -> "Align": + def center( + cls, + renderable: "RenderableType", + style: StyleType = None, + *, + pad: bool = True, + width: int = None, + ) -> "Align": """Align a renderable to the center.""" - return cls(renderable, "center", style=style) + return cls(renderable, "center", style=style, pad=pad, width=width) @classmethod - def right(cls, renderable: "RenderableType", style: StyleType = None) -> "Align": + def right( + cls, + renderable: "RenderableType", + style: StyleType = None, + *, + pad: bool = True, + width: int = None, + ) -> "Align": """Align a renderable to the right.""" - return cls(renderable, "right", style=style) + return cls(renderable, "right", style=style, pad=pad, width=width) def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": align = self.align - rendered = console.render(self.renderable, options) + rendered = console.render(Constrain(self.renderable, width=self.width), options) lines = list(Segment.split_lines(rendered)) width, height = Segment.get_shape(lines) lines = Segment.set_shape(lines, width, height) new_line = Segment.line() excess_space = options.max_width - width - def generate_segments(): + def generate_segments() -> Iterable[Segment]: if excess_space <= 0: # Exact fit for line in lines: @@ -72,17 +104,18 @@ def generate_segments(): elif align == "left": # Pad on the right - pad = Segment(" " * excess_space) + pad = Segment(" " * excess_space) if self.pad else None for line in lines: yield from line - yield pad + if pad: + yield pad yield new_line elif align == "center": # Pad left and right left = excess_space // 2 pad = Segment(" " * left) - pad_right = Segment(" " * (excess_space - left)) + pad_right = Segment(" " * (excess_space - left)) if self.pad else None for line in lines: if left: yield pad diff --git a/rich/box.py b/rich/box.py index 6e91321e1..a4cf672d4 100644 --- a/rich/box.py +++ b/rich/box.py @@ -81,7 +81,7 @@ def get_top(self, widths: Iterable[int]) -> str: def get_row( self, widths: Iterable[int], - level: Literal["head", "row", "foot"] = "row", + level: Literal["head", "row", "foot", "mid"] = "row", edge: bool = True, ) -> str: """Get the top of a simple box. @@ -102,6 +102,11 @@ def get_row( horizontal = self.row_horizontal cross = self.row_cross right = self.row_right + elif level == "mid": + left = self.mid_left + horizontal = " " + cross = self.mid_vertical + right = self.mid_right elif level == "foot": left = self.foot_row_left horizontal = self.foot_row_horizontal diff --git a/rich/constrain.py b/rich/constrain.py index ec7828a04..a16ea4b84 100644 --- a/rich/constrain.py +++ b/rich/constrain.py @@ -1,9 +1,11 @@ -from typing import Optional +from typing import Optional, TYPE_CHECKING -from .console import Console, ConsoleOptions, RenderableType, RenderResult from .jupyter import JupyterMixin from .measure import Measurement +if TYPE_CHECKING: + from .console import Console, ConsoleOptions, RenderableType, RenderResult + class Constrain(JupyterMixin): """Constrain the width of a renderable to a given number of characters. @@ -13,20 +15,20 @@ class Constrain(JupyterMixin): width (int, optional): The maximum width (in characters) to render. Defaults to 80. """ - def __init__(self, renderable: RenderableType, width: Optional[int] = 80) -> None: + def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None: self.renderable = renderable self.width = width def __rich_console__( - self, console: Console, options: ConsoleOptions - ) -> RenderResult: + self, console: "Console", options: "ConsoleOptions" + ) -> "RenderResult": if self.width is None: yield self.renderable else: child_options = options.update(width=min(self.width, options.max_width)) yield from console.render(self.renderable, child_options) - def __rich_measure__(self, console: Console, max_width: int) -> Measurement: + def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": if self.width is None: return Measurement.get(console, self.renderable, max_width) else: diff --git a/rich/panel.py b/rich/panel.py index 6b5fa58b7..ca2caee3c 100644 --- a/rich/panel.py +++ b/rich/panel.py @@ -2,6 +2,7 @@ from .box import get_safe_box, Box, SQUARE, ROUNDED +from .align import AlignValues from .console import ( Console, ConsoleOptions, @@ -12,7 +13,7 @@ from .jupyter import JupyterMixin from .padding import Padding, PaddingDimensions from .style import Style, StyleType -from .text import Text +from .text import Text, TextType from .segment import Segment @@ -40,6 +41,8 @@ def __init__( renderable: RenderableType, box: Box = ROUNDED, *, + title: TextType = None, + title_align: AlignValues = "center", safe_box: Optional[bool] = None, expand: bool = True, style: StyleType = "none", @@ -49,6 +52,8 @@ def __init__( ) -> None: self.renderable = renderable self.box = box + self.title = title + self.title_align = title_align self.safe_box = safe_box self.expand = expand self.style = style @@ -62,6 +67,8 @@ def fit( renderable: RenderableType, box: Box = ROUNDED, *, + title: TextType = None, + title_align: AlignValues = "center", safe_box: Optional[bool] = None, style: StyleType = "none", border_style: StyleType = "none", @@ -72,6 +79,8 @@ def fit( return cls( renderable, box, + title=title, + title_align=title_align, safe_box=safe_box, style=style, border_style=border_style, @@ -108,7 +117,24 @@ def __rich_console__( line_start = Segment(box.mid_left, border_style) line_end = Segment(f"{box.mid_right}", border_style) new_line = Segment.line() - yield Segment(box.get_top([width - 2]), border_style) + if self.title is None: + yield Segment(box.get_top([width - 2]), border_style) + else: + title_text = ( + Text.from_markup(self.title) + if isinstance(self.title, str) + else self.title.copy() + ) + title_text.style = border_style + title_text.end = "" + title_text.plain = title_text.plain.replace("\n", " ") + title_text = title_text.tabs_to_spaces() + title_text.pad(1) + title_text.align(self.title_align, width - 4, character=box.top) + yield Segment(box.top_left + box.top, border_style) + yield title_text + yield Segment(box.top + box.top_right, border_style) + yield new_line for line in lines: yield line_start @@ -129,7 +155,7 @@ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: c = Console() from .padding import Padding - from .box import ROUNDED + from .box import ROUNDED, DOUBLE p = Panel( Panel.fit( @@ -138,7 +164,8 @@ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: safe_box=True, style="on red", ), - border_style="on blue", + title="[b]Hello, World", + box=DOUBLE, ) print(p) diff --git a/rich/rule.py b/rich/rule.py index 0e0851164..5c8918ca5 100644 --- a/rich/rule.py +++ b/rich/rule.py @@ -8,11 +8,13 @@ class Rule(JupyterMixin): - """A console renderable to draw a horizontal rule (line). + r"""A console renderable to draw a horizontal rule (line). Args: title (Union[str, Text], optional): Text to render in the rule. Defaults to "". character (str, optional): Character used to draw the line. Defaults to "─". + style (StyleType, optional): Style of Rule. Defaults to "rule.line". + end (str, optional): Character at end of Rule. defaults to "\\n" """ def __init__( @@ -21,12 +23,14 @@ def __init__( *, character: str = None, style: Union[str, Style] = "rule.line", + end: str = "\n", ) -> None: if character and cell_len(character) != 1: raise ValueError("'character' argument must have a cell width of 1") self.title = title self.character = character self.style = style + self.end = end def __repr__(self) -> str: return f"Rule({self.title!r}, {self.character!r})" @@ -51,7 +55,7 @@ def __rich_console__( title_text.plain = title_text.plain.replace("\n", " ") title_text = title_text.tabs_to_spaces() - rule_text = Text() + rule_text = Text(end=self.end) center = (width - cell_len(title_text.plain)) // 2 rule_text.append(character * (center - 1) + " ", self.style) rule_text.append(title_text) diff --git a/rich/table.py b/rich/table.py index f239580a1..afe3b6de3 100644 --- a/rich/table.py +++ b/rich/table.py @@ -109,6 +109,7 @@ class Table(JupyterMixin): show_footer (bool, optional): Show a footer row. Defaults to False. show_edge (bool, optional): Draw a box around the outside of the table. Defaults to True. show_lines (bool, optional): Draw lines between every row. Defaults to False. + leading (bool, optional): Number of blank lines between rows (precludes ``show_lines``). Defaults to 0. style (Union[str, Style], optional): Default style for the table. Defaults to "none". row_styles (List[Union, str], optional): Optional list of row styles, if more that one style is give then the styles will alternate. Defaults to None. header_style (Union[str, Style], optional): Style of the header. Defaults to None. @@ -136,6 +137,7 @@ def __init__( show_footer: bool = False, show_edge: bool = True, show_lines: bool = False, + leading: int = 0, style: StyleType = "none", row_styles: Iterable[StyleType] = None, header_style: StyleType = None, @@ -161,6 +163,7 @@ def __init__( self.show_footer = show_footer self.show_edge = show_edge self.show_lines = show_lines + self.leading = leading self.collapse_padding = collapse_padding self.style = style self.header_style = header_style @@ -593,6 +596,7 @@ def _render( show_footer = self.show_footer show_edge = self.show_edge show_lines = self.show_lines + leading = self.leading _Segment = Segment if _box: @@ -685,15 +689,22 @@ def _render( _box.get_row(widths, "head", edge=show_edge), border_style ) yield new_line - if _box and show_lines: + if _box and (show_lines or leading): if ( not last and not (show_footer and index >= len(rows) - 2) and not (show_header and header_row) ): - yield _Segment( - _box.get_row(widths, "row", edge=show_edge), border_style - ) + if leading: + for _ in range(leading): + yield _Segment( + _box.get_row(widths, "mid", edge=show_edge), + border_style, + ) + else: + yield _Segment( + _box.get_row(widths, "row", edge=show_edge), border_style + ) yield new_line if _box and show_edge: diff --git a/rich/text.py b/rich/text.py index b59296d57..46e6b5e79 100644 --- a/rich/text.py +++ b/rich/text.py @@ -16,6 +16,7 @@ from ._loop import loop_last from ._wrap import divide_line +from .align import AlignValues from .cells import cell_len, set_cell_size from .containers import Lines from .control import strip_control_codes @@ -617,6 +618,15 @@ def _trim_spans(self) -> None: append(span) self._spans[:] = spans + def pad(self, count: int, character: str = " ") -> None: + """Pad left and right with a given number of characters. + + Args: + count (int): Width of padding. + """ + self.pad_left(count, character=character) + self.pad_right(count, character=character) + def pad_left(self, count: int, character: str = " ") -> None: """Pad the left with a given character. @@ -640,6 +650,26 @@ def pad_right(self, count: int, character: str = " ") -> None: if count: self.plain = f"{self.plain}{character * count}" + def align(self, align: AlignValues, width: int, character: str = " ") -> None: + """Align text to a given width. + + Args: + align (AlignValues): One of "left", "center", or "right". + width (int): Desired width. + character (str, optional): Character to pad with. Defaults to " ". + """ + self.truncate(width) + excess_space = width - cell_len(self.plain) + if excess_space: + if align == "left": + self.pad_right(excess_space, character) + elif align == "center": + left = excess_space // 2 + self.pad_left(left, character) + self.pad_right(excess_space - left, character) + else: + self.pad_left(excess_space, character) + def append( self, text: Union["Text", str], style: Union[str, "Style"] = None ) -> "Text": diff --git a/tests/test_align.py b/tests/test_align.py index 6dc3f2702..fe43ea9e6 100644 --- a/tests/test_align.py +++ b/tests/test_align.py @@ -64,6 +64,22 @@ def test_measure(): assert _max == 7 +def test_align_no_pad(): + console = Console(file=io.StringIO(), width=10) + console.print(Align("foo", "center", pad=False)) + console.print(Align("foo", "left", pad=False)) + assert console.file.getvalue() == " foo\nfoo\n" + + +def test_align_width(): + console = Console(file=io.StringIO(), width=40) + words = "Deep in the human unconscious is a pervasive need for a logical universe that makes sense. But the real universe is always one step beyond logic" + console.print(Align(words, "center", width=30)) + result = console.file.getvalue() + expected = " Deep in the human unconscious \n is a pervasive need for a \n logical universe that makes \n sense. But the real universe \n is always one step beyond \n logic \n" + assert result == expected + + def test_shortcuts(): assert Align.left("foo").align == "left" assert Align.left("foo").renderable == "foo" diff --git a/tests/test_panel.py b/tests/test_panel.py index 6f8d60f92..ea5057ead 100644 --- a/tests/test_panel.py +++ b/tests/test_panel.py @@ -10,6 +10,7 @@ Panel.fit("Hello, World"), Panel("Hello, World", width=8), Panel(Panel("Hello, World")), + Panel("Hello, World", title="FOO"), ] expected = [ @@ -18,6 +19,7 @@ "╭────────────╮\n│Hello, World│\n╰────────────╯\n", "╭──────╮\n│Hello,│\n│World │\n╰──────╯\n", "╭────────────────────────────────────────────────╮\n│╭──────────────────────────────────────────────╮│\n││Hello, World ││\n│╰──────────────────────────────────────────────╯│\n╰────────────────────────────────────────────────╯\n", + "╭───────────────────── FOO ──────────────────────╮\n│Hello, World │\n╰────────────────────────────────────────────────╯\n", ] diff --git a/tests/test_table.py b/tests/test_table.py index 20beb3510..d54c6a0a1 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -64,11 +64,22 @@ def render_tables(): assert Measurement.get(console, table, 80) == Measurement(20, 20) console.print(table) + table.columns[0].no_wrap = True + table.columns[1].no_wrap = True + table.columns[2].no_wrap = True + + console.print(table) + + table.padding = 0 + table.width = 60 + table.leading = 1 + console.print(table) + return console.file.getvalue() def test_render_table(): - expected = "test table\n┏━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━╇╇┩\n│ Ave… │││\n│ │││\n└──────┴┴┘\n table \n caption \n test table \n┏━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━╇╇┩\n│ Averlong… │││\n│ │││\n└───────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━━━━━━╇╇┩\n│ Averlongwordg… │││\n│ │││\n└────────────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━━━━━━━━━━━╇╇┩\n│ Averlongwordgoeshe… │││\n│ │││\n└─────────────────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━┳━━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━╇━━┩\n│ Averlongwordgoeshere │ │ │\n│ │ │ │\n└──────────────────────┴──┴──┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━┩\n│ Averlongwordgoeshere │ ba… │ │\n│ │ pa… │ │\n└──────────────────────┴─────┴────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancak… │ │\n└──────────────────────┴─────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancakes │ │\n└──────────────────────┴──────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└───────────────────────┴──────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└──────────────────────────┴────────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n┃ foo ┃ bar ┃ baz ┃ \n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n│ Averlongwordgoeshere │ banana pancakes │ │ \n└──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n ┃ foo ┃ bar ┃ baz ┃ \n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n │ Averlongwordgoeshere │ banana pancakes │ │ \n └──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓\n ┃ foo ┃ bar ┃ baz ┃\n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩\n │ Averlongwordgoeshere │ banana pancakes │ │\n └──────────────────────┴─────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n│ Coffee │ │ │ │\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ total │ │ │ │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n foo ┃ bar ┃ baz ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n Averlongwordgoeshere │ banana pancakes │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ Chocolate │ │ cinnamon \n──────────────────────┼─────────────────┼─────┼──────────\n total │ │ │ \n table caption \n test table \n ┃ ┃ ┃ \n foo ┃ bar ┃ baz ┃ \n ┃ ┃ ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n │ │ │ \n Averlongwordgoeshere │ banana pancakes │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ Chocolate │ │ cinnamon \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n total │ │ │ \n │ │ │ \n table caption \n test table \n ┃┃┃\n foo ┃┃┃\n ┃┃┃\n━━━━━━━━━━━━━━━━━╇╇╇\n │││\n Averlongwordgo… │││\n │││\n │││\n─────────────────┼┼┼\n │││\n Coffee │││\n │││\n─────────────────┼┼┼\n │││\n Coffee │││\n │││\n─────────────────┼┼┼\n │││\n total │││\n │││\n table caption \n" + expected = "test table\n┏━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━╇╇┩\n│ Ave… │││\n│ │││\n└──────┴┴┘\n table \n caption \n test table \n┏━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━╇╇┩\n│ Averlong… │││\n│ │││\n└───────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━━━━━━╇╇┩\n│ Averlongwordg… │││\n│ │││\n└────────────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━┳┳┓\n┃ foo ┃┃┃\n┡━━━━━━━━━━━━━━━━━━━━━╇╇┩\n│ Averlongwordgoeshe… │││\n│ │││\n└─────────────────────┴┴┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━┳━━┓\n┃ foo ┃ ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━╇━━┩\n│ Averlongwordgoeshere │ │ │\n│ │ │ │\n└──────────────────────┴──┴──┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━┩\n│ Averlongwordgoeshere │ ba… │ │\n│ │ pa… │ │\n└──────────────────────┴─────┴────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancak… │ │\n└──────────────────────┴─────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancakes │ │\n└──────────────────────┴──────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└───────────────────────┴──────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└──────────────────────────┴────────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n┃ foo ┃ bar ┃ baz ┃ \n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n│ Averlongwordgoeshere │ banana pancakes │ │ \n└──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓ \n ┃ foo ┃ bar ┃ baz ┃ \n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩ \n │ Averlongwordgoeshere │ banana pancakes │ │ \n └──────────────────────┴─────────────────┴─────┘ \n table caption \n test table \n ┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┓\n ┃ foo ┃ bar ┃ baz ┃\n ┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━┩\n │ Averlongwordgoeshere │ banana pancakes │ │\n └──────────────────────┴─────────────────┴─────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n│ Coffee │ │ │ │\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━┓\n┃ foo ┃ bar ┃ baz ┃ ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ │ │ │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ Coffee │ Chocolate │ │ cinnamon │\n├──────────────────────┼─────────────────┼─────┼──────────┤\n│ total │ │ │ │\n└──────────────────────┴─────────────────┴─────┴──────────┘\n table caption \n test table \n foo ┃ bar ┃ baz ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n Averlongwordgoeshere │ banana pancakes │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n Coffee │ Chocolate │ │ cinnamon \n──────────────────────┼─────────────────┼─────┼──────────\n total │ │ │ \n table caption \n test table \n ┃ ┃ ┃ \n foo ┃ bar ┃ baz ┃ \n ┃ ┃ ┃ \n━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━\n │ │ │ \n Averlongwordgoeshere │ banana pancakes │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ │ │ \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n Coffee │ Chocolate │ │ cinnamon \n │ │ │ \n──────────────────────┼─────────────────┼─────┼──────────\n │ │ │ \n total │ │ │ \n │ │ │ \n table caption \n test table \n ┃┃┃\n foo ┃┃┃\n ┃┃┃\n━━━━━━━━━━━━━━━━━╇╇╇\n │││\n Averlongwordgo… │││\n │││\n │││\n─────────────────┼┼┼\n │││\n Coffee │││\n │││\n─────────────────┼┼┼\n │││\n Coffee │││\n │││\n─────────────────┼┼┼\n │││\n total │││\n │││\n table caption \n test table \n ┃ ┃┃\n foo ┃ bar ┃┃\n ┃ ┃┃\n━━━━━━━━━━╇━━━━━━━━━╇╇\n │ ││\n Averlon… │ banana… ││\n │ ││\n──────────┼─────────┼┼\n │ ││\n Coffee │ ││\n │ ││\n──────────┼─────────┼┼\n │ ││\n Coffee │ Chocol… ││\n │ ││\n──────────┼─────────┼┼\n │ ││\n total │ ││\n │ ││\n table caption \n test table \nfoo ┃ bar ┃ baz┃ \n━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━╇━━━━━━━━━\nAverlongwordgoeshere │ banana pancakes │ │ \n │ │ │ \nCoffee │ │ │ \n │ │ │ \nCoffee │ Chocolate │ │cinnamon \n─────────────────────────┼───────────────────┼────┼─────────\ntotal │ │ │ \n table caption \n" assert render_tables() == expected diff --git a/tests/test_text.py b/tests/test_text.py index c05e9ea82..432bc4a91 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -517,3 +517,27 @@ def test_truncate_ellipsis_pad(input, count, expected): text = Text(input) text.truncate(count, overflow="ellipsis", pad=True) assert text.plain == expected + + +def test_pad(): + test = Text("foo") + test.pad(2) + assert test.plain == " foo " + + +def test_align_left(): + test = Text("foo") + test.align("left", 10) + assert test.plain == "foo " + + +def test_align_right(): + test = Text("foo") + test.align("right", 10) + assert test.plain == " foo" + + +def test_align_center(): + test = Text("foo") + test.align("center", 10) + assert test.plain == " foo "