From 6dfdf1a40c8f02a228b9d21b3bce08c86bde5c24 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 7 Aug 2020 23:14:52 +0100 Subject: [PATCH] added pretty functionality --- CHANGELOG.md | 11 +- poetry.lock | 23 +--- pyproject.toml | 3 +- rich/console.py | 20 +-- rich/default_styles.py | 2 + rich/pretty.py | 282 +++++++++++++++++++++++++++++++---------- rich/prompt.py | 4 +- rich/scope.py | 2 +- rich/table.py | 6 + rich/text.py | 89 ++++++++----- tests/test_columns.py | 2 +- tests/test_table.py | 2 +- 12 files changed, 316 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5de442ab..1470d1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,22 @@ 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). -## [5.0.1] - Unreleased +## [5.1.0] - Unreleased + +### Added + +- Added Text.cell_len +- Added helpful message regarding unicode decoding errors ### Fixed - Fixed deprecation warnings re backslash https://github.com/willmcgugan/rich/issues/210 - Fixed repr highlighting of scientific notation, e.g. 1e100 +### Changed + +- Implemented pretty printing, and removed pprintpp from dependancies + ## [5.0.0] - 2020-08-02 ### Changed diff --git a/poetry.lock b/poetry.lock index 132f4a23..1e40a254 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,7 +1,7 @@ [[package]] category = "main" description = "Disable App Nap on OS X 10.9" -marker = "python_version >= \"3.3\" and sys_platform == \"darwin\" or platform_system == \"Darwin\"" +marker = "sys_platform == \"darwin\" or platform_system == \"Darwin\" or python_version >= \"3.3\" and sys_platform == \"darwin\"" name = "appnope" optional = true python-versions = "*" @@ -24,7 +24,6 @@ tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.i [[package]] category = "main" description = "Specifications for callback functions passed in to an API" -marker = "python_version >= \"3.3\"" name = "backcall" optional = true python-versions = "*" @@ -193,7 +192,6 @@ test = ["pytest (>=3.6.0)", "pytest-cov", "mock"] [[package]] category = "main" description = "An autocompletion tool for Python that can be used for text editors." -marker = "python_version >= \"3.3\"" name = "jedi" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -394,7 +392,7 @@ testing = ["docopt", "pytest (>=3.0.7)"] [[package]] category = "main" description = "Pexpect allows easy control of interactive console applications." -marker = "python_version >= \"3.3\" and sys_platform != \"win32\"" +marker = "python_version >= \"3.3\" and sys_platform != \"win32\" or sys_platform != \"win32\"" name = "pexpect" optional = true python-versions = "*" @@ -406,20 +404,11 @@ ptyprocess = ">=0.5" [[package]] category = "main" description = "Tiny 'shelve'-like database with concurrency support" -marker = "python_version >= \"3.3\"" name = "pickleshare" optional = true python-versions = "*" version = "0.7.5" -[[package]] -category = "main" -description = "A drop-in replacement for pprint that's actually pretty" -name = "pprintpp" -optional = false -python-versions = "*" -version = "0.4.0" - [[package]] category = "main" description = "Python client for the Prometheus monitoring system." @@ -434,7 +423,6 @@ twisted = ["twisted"] [[package]] category = "main" description = "Library for building powerful interactive command lines in Python" -marker = "python_version >= \"3.3\"" name = "prompt-toolkit" optional = true python-versions = ">=3.6" @@ -633,7 +621,8 @@ testing = ["jaraco.itertools", "func-timeout"] jupyter = ["ipywidgets"] [metadata] -content-hash = "e2c24649506040755aa391e68196f00a65ec8e7f7130feb9254a3b6d6352bb72" +content-hash = "64c8b886ebf3d4fec7647f65af7ebf6b20e031fbec205d96af3c937ff686c415" +lock-version = "1.0" python-versions = "^3.6" [metadata.files] @@ -787,10 +776,6 @@ pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] -pprintpp = [ - {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, - {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, -] prometheus-client = [ {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, diff --git a/pyproject.toml b/pyproject.toml index c6bc6cd6..2f21f017 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 = "5.0.1" +version = "5.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" @@ -23,7 +23,6 @@ include = ["rich/py.typed"] [tool.poetry.dependencies] python = "^3.6" -pprintpp = "^0.4.0" typing-extensions = "^3.7.4" dataclasses = {version="^0.7", python = "~3.6"} pygments = "^2.6.0" diff --git a/rich/console.py b/rich/console.py index ef6e53a9..a66e92aa 100644 --- a/rich/console.py +++ b/rich/console.py @@ -984,14 +984,18 @@ class Console: else: text = self._render_buffer() if text: - if WINDOWS: # pragma: no cover - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): - write(line) - else: - self.file.write(text) - self.file.flush() + try: + if WINDOWS: # pragma: no cover + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + write(line) + else: + self.file.write(text) + self.file.flush() + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise def _render_buffer(self) -> str: """Render buffered output, and clear buffer.""" diff --git a/rich/default_styles.py b/rich/default_styles.py index 367e9bcc..d317d7f2 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -47,8 +47,10 @@ DEFAULT_STYLES: Dict[str, Style] = { "log.time": Style(color="cyan", dim=True), "log.message": Style(), "log.path": Style(dim=True), + "repr.error": Style(color="red", bold=True), "repr.str": Style(color="green", italic=False, bold=False), "repr.brace": Style(bold=True), + "repr.comma": Style(bold=True), "repr.tag_start": Style(bold=True), "repr.tag_name": Style(color="bright_magenta", bold=True), "repr.tag_contents": Style(color="default"), diff --git a/rich/pretty.py b/rich/pretty.py index 4f50efbd..a4c67938 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -1,9 +1,12 @@ -from dataclasses import dataclass +import sys -from typing import Any, Iterable, List, Optional, Tuple, TYPE_CHECKING +from dataclasses import dataclass, field +from rich.highlighter import ReprHighlighter -from pprintpp import pformat +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING +from .cells import cell_len +from .highlighter import Highlighter, NullHighlighter, ReprHighlighter from ._loop import loop_last from .measure import Measurement from .text import Text @@ -12,101 +15,250 @@ if TYPE_CHECKING: # pragma: no cover from .console import Console, ConsoleOptions, HighlighterType, RenderResult +def install(console: "Console" = None) -> None: + """Install automatic pretty printing in the Python REPL.""" + from rich import get_console + + console = console or get_console() + + def display_hook(value: Any) -> None: + if value is not None: + console.print( + value + if hasattr(value, "__rich_console__") or hasattr(value, "__rich__") + else pretty_repr(value) + ) + + sys.displayhook = display_hook + + class Pretty: + """A rich renderable that pretty prints an object.""" + def __init__(self, _object: Any, highlighter: "HighlighterType" = None) -> None: self._object = _object - self.highlighter = highlighter or Text + self.highlighter = highlighter or NullHighlighter() def __rich_console__( self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": - # TODO: pformat tends to render a smaller width than it needs to, investigate why - print(options) - _min, max_width = Measurement.get(console, self, options.max_width) - pretty_str = pformat(self._object, width=max_width) - pretty_str = pretty_str.replace("\r", "") - pretty_text = self.highlighter(pretty_str) + pretty_text = pretty_repr(self._object, max_width=options.max_width) yield pretty_text def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": - pretty_str = pformat(self._object, width=max_width) - pretty_str = pretty_str.replace("\r", "") - text = Text(pretty_str) - measurement = Measurement.get(console, text, max_width) - print(measurement) - return measurement - - -@dataclass -class _Node: - indent: int = -1 - object_repr: Optional[str] = None - name: str = "" - braces: Optional[Tuple[str, str]] = None - values: Optional[List["_Node"]] = None - items: Optional[List[Tuple[str, "_Node"]]] = None - expanded: bool = False + pretty_text = pretty_repr(self._object, max_width=max_width) + text_width = max(cell_len(line) for line in pretty_text.plain.splitlines()) + return Measurement(text_width, text_width) _BRACES = { - list: ("", "[", "]"), - tuple: ("", "(", ")"), - set: ("", "{", "}"), - frozenset: ("frozenset", "({", "})"), + dict: (Text("{", "repr.brace"), Text("}", "repr.brace")), + frozenset: ( + Text.assemble("frozenset(", ("{", "repr.brace")), + Text.assemble(("}", "repr.brace"), ")"), + ), + list: (Text("[", "repr.brace"), Text("]", "repr.brace")), + set: (Text("{", "repr.brace"), Text("}", "repr.brace")), + tuple: (Text("(", "repr.brace"), Text(")", "repr.brace")), +} +_CONTAINERS = tuple(_BRACES.keys()) +_REPR_STYLES = { + type(None): "repr.none", + str: "repr.str", + float: "repr.number", + int: "repr.number", } @dataclass -class _Value: - object_repr: str +class _Line: + """A line in a pretty repr.""" + + parts: List[Text] = field(default_factory=list) + _cell_len: int = 0 + + def append(self, text: Text) -> None: + """Add text to line.""" + # Efficiently keep track of cell length + self.parts.append(text) + self._cell_len += text.cell_len + + @property + def cell_len(self) -> int: + return ( + self._cell_len + if self.parts and not self.parts[-1].plain.endswith(" ") + else self._cell_len - 1 + ) + + @property + def text(self): + """The text as a while.""" + return Text("").join(self.parts) -@dataclass -class _Container: - braces: Tuple[str] +def pretty_repr( + _object: Any, + *, + max_width: Optional[int] = 80, + indent_size: int = 4, + highlighter: Highlighter = None, +) -> Text: + """Return a 'pretty' repr. + Args: + _object (Any): Object to repr. + max_width (int, optional): Maximum desired width. Defaults to 80. + indent_size (int, optional): Number of spaces in an indent. Defaults to 4. + highlighter (Highlighter, optional): A highlighter for repr strings. Defaults to ReprHighlighter. -def pretty_repr(_object: Any, *, width: int = 80, indent_size: int = 4) -> str: + Returns: + Text: A Text instance conaining a pretty repr. + """ + + class MaxLineReached(Exception): + """Line is greater than maximum""" + + def __init__(self, line_no: int) -> None: + self.line_no = line_no + super().__init__() + + if highlighter is None: + highlighter = ReprHighlighter() indent = " " * indent_size - stack = [] - push = stack.append + expand_level = 0 - node = _object + lines: List[_Line] = [_Line()] - if isinstance(node, (tuple, list, set, dict)): - braces = _BRACES[type(node)] - push(_Container()) + visited_set: Set[int] = set() + repr_cache: Dict[int, Text] = {} + repr_cache_get = repr_cache.get - def add_node(parent_node: _Node, node_object: Any) -> _Node: - if isinstance(node_object, (list, set, frozenset, tuple)): - name, open_brace, close_brace = _BRACES[type(node_object)] - node = _Node( - indent=parent_node.indent + 1, braces=(open_brace, close_brace) - ) - node.values = [add_node(node, child) for child in node_object] - elif isinstance(node_object, dict): - node = _Node(indent=parent_node.indent + 1, braces=("{", "}")) - node.items = [ - (key, add_node(node, value)) for key, value in node_object.items() - ] + def to_repr_text(node: Any) -> Text: + node_id = id(node) + cached = repr_cache_get(node_id) + if cached is not None: + return cached + style: Optional[str] + if node is True: + style = "repr.bool_true" + elif node is False: + style = "repr.bool_false" else: - node = _Node(indent=parent_node.indent + 1, object_repr=repr(node_object)) - return node + style = _REPR_STYLES.get(type(node)) + try: + repr_text = repr(node) + except Exception as error: + text = Text(f"", "repr.error") + else: + if style is None: + text = highlighter(Text(repr_text)) + else: + text = Text(repr_text, style) + repr_cache[node_id] = text + visited_set.add(node_id) + return text - node = add_node(_Node(), _object) - print(node) - node.expanded = True + comma = Text(", ") + colon = Text(": ") + line_break: Optional[int] = None - output = [] + def traverse(node: Any, level: int = 0) -> None: + nonlocal line_break - return "" + append_line = lines.append + + def append_text(text: Text) -> None: + nonlocal line_break + line = lines[-1] + line.append(text) + if max_width is not None and line.cell_len > max_width: + if line_break is not None and len(lines) <= line_break: + return + line_break = len(lines) + raise MaxLineReached(level) + + node_id = id(node) + if node_id in visited_set: + append_text(Text("...", "repr.error")) + return + + visited_set.add(node_id) + + if type(node) in _CONTAINERS: + brace_open, brace_close = _BRACES[type(node)] + expanded = level < expand_level + + append_text(brace_open) + if isinstance(node, dict): + for last, (key, value) in loop_last(node.items()): + if expanded: + append_line(_Line()) + append_text(Text(indent * (level + 1))) + append_text(to_repr_text(key)) + append_text(colon) + traverse(value, level + 1) + if not last: + append_text(comma) + else: + for last, value in loop_last(node): + if expanded: + append_line(_Line()) + append_text(Text(indent * (level + 1))) + traverse(value, level + 1) + if not last: + append_text(comma) + if expanded: + lines.append(_Line()) + append_text(Text.assemble(f"{indent * level}", brace_close)) + else: + append_text(brace_close) + else: + append_text(to_repr_text(node)) + + visited_set.remove(node_id) + + while True: + try: + traverse(_object) + except MaxLineReached as max_line: + del lines[:] + visited_set.clear() + lines.append(_Line()) + expand_level += 1 + else: + break # pragma: no cover + + return Text("\n").join(line.text for line in lines) if __name__ == "__main__": - data = {"d": [1, "Hello World!", 2, 3, 4, {5, 6, 7, (1, 2, 3, 4), 8}]} + from collections import defaultdict + + class BrokenRepr: + def __repr__(self): + 1 / 0 + + d = defaultdict(int) + d["foo"] = 5 + data = { + "foo": [1, "Hello World!", 2, 3, 4, {5, 6, 7, (1, 2, 3, 4), 8}], + "bar": frozenset({1, 2, 3}), + False: "This is false", + True: "This is true", + None: "This is None", + "Broken": BrokenRepr(), + } + data["foo"].append(data) + + from rich.console import Console + + console = Console() + print("-" * console.width) from rich import print - print(pretty_repr(data)) - + p = Pretty(data) + print(Measurement.get(console, p)) + console.print(p) diff --git a/rich/prompt.py b/rich/prompt.py index 41ee73dc..ebabcfa4 100644 --- a/rich/prompt.py +++ b/rich/prompt.py @@ -13,11 +13,11 @@ class PromptError(Exception): class InvalidResponse(PromptError): - """Exception to indicate a response was invalid. Raise this within process_respons to indicate an error + """Exception to indicate a response was invalid. Raise this within process_response() to indicate an error and provide an error message. Args: - message (str): Error message. + message (Union[str, Text]): Error message. """ def __init__(self, message: TextType) -> None: diff --git a/rich/scope.py b/rich/scope.py index 922c6329..fb26b8f2 100644 --- a/rich/scope.py +++ b/rich/scope.py @@ -27,7 +27,7 @@ def render_scope( RenderableType: A renderable object. """ highlighter = ReprHighlighter() - items_table = Table.grid(padding=(0, 1)) + items_table = Table.grid(padding=(0, 1), expand=False) items_table.add_column(justify="right") def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]: diff --git a/rich/table.py b/rich/table.py index edb834b8..1b99266d 100644 --- a/rich/table.py +++ b/rich/table.py @@ -432,6 +432,12 @@ class Table(JupyterMixin): widths = ratio_reduce(excess_width, [1] * len(widths), widths, widths) table_width = sum(widths) + width_ranges = [ + self._measure_column(console, column, width) + for width, column in zip(widths, columns) + ] + widths = [_range.maximum or 1 for _range in width_ranges] + if table_width < max_width and self.expand: pad_widths = ratio_distribute(max_width - table_width, widths) widths = [_width + pad for _width, pad in zip(widths, pad_widths)] diff --git a/rich/text.py b/rich/text.py index 625f78f6..ef1afeb9 100644 --- a/rich/text.py +++ b/rich/text.py @@ -180,6 +180,11 @@ class Text(JupyterMixin): return other.plain in self.plain return False + @property + def cell_len(self) -> int: + """Get the number of cells required to render this text.""" + return cell_len(self.plain) + @classmethod def from_markup( cls, @@ -560,11 +565,16 @@ class Text(JupyterMixin): """ new_text = self.blank_copy() - append = new_text.append - for last, line in loop_last(lines): - append(line) - if not last: - append(self) + append = new_text.append_text + if self.plain: + for last, line in loop_last(lines): + append(line) + if not last: + append(self) + else: + for line in lines: + append(line) + return new_text def tabs_to_spaces(self, tab_size: int = None) -> "Text": @@ -716,38 +726,57 @@ class Text(JupyterMixin): style (str, optional): A style name. Defaults to None. Returns: - text (Text): Returns self for chaining. + Text: Returns self for chaining. """ if not isinstance(text, (str, Text)): raise TypeError("Only str or Text can be appended to Text") - if not len(text): - return self - if isinstance(text, str): - text = strip_control_codes(text) - self._text.append(text) - offset = len(self) - text_length = len(text) - if style is not None: - self._spans.append(Span(offset, offset + text_length, style)) - self._length += text_length - elif isinstance(text, Text): - _Span = Span - if style is not None: - raise ValueError("style must not be set when appending Text instance") + if len(text): + if isinstance(text, str): + text = strip_control_codes(text) + self._text.append(text) + offset = len(self) + text_length = len(text) + if style is not None: + self._spans.append(Span(offset, offset + text_length, style)) + self._length += text_length + elif isinstance(text, Text): + _Span = Span + if style is not None: + raise ValueError( + "style must not be set when appending Text instance" + ) - text_length = self._length - if text.style is not None: - self._spans.append( - _Span(text_length, text_length + len(text), text.style) + text_length = self._length + if text.style is not None: + self._spans.append( + _Span(text_length, text_length + len(text), text.style) + ) + self._text.append(text.plain) + self._spans.extend( + _Span(start + text_length, end + text_length, style) + for start, end, style in text._spans ) - self._text.append(text.plain) - self._spans.extend( - _Span(start + text_length, end + text_length, style) - for start, end, style in text._spans - ) - self._length += len(text) + self._length += len(text) + return self + + def append_text(self, text: "Text") -> "Text": + """Append another Text instance. This method is more performant that Text.append. + + Returns: + Text: Returns self for chaining. + """ + _Span = Span + text_length = self._length + if text.style is not None: + self._spans.append(_Span(text_length, text_length + len(text), text.style)) + self._text.append(text.plain) + self._spans.extend( + _Span(start + text_length, end + text_length, style) + for start, end, style in text._spans + ) + self._length += len(text) return self def copy_styles(self, text: "Text") -> None: diff --git a/tests/test_columns.py b/tests/test_columns.py index ebb7e257..3df14197 100644 --- a/tests/test_columns.py +++ b/tests/test_columns.py @@ -61,7 +61,7 @@ def render(): def test_render(): - expected = "────────────────────────────────────────────── empty ───────────────────────────────────────────────\n───────────────────────────────────────────── optimal ──────────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n───────────────────────────────────────── optimal, expand ──────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n────────────────────────────────────── columm first, optimal ───────────────────────────────────────\nUrsus americanus American marten Scolopax minor Ant \nAmerican buffalo Martes americana Anaconda (unidentified) Anteater, australian spiny\nBison bison American racer Eunectes sp. Tachyglossus aculeatus \nAmerican crow Coluber constrictor Andean goose Anteater, giant \nCorvus brachyrhynchos American woodcock Chloephaga melanoptera Myrmecophaga tridactyla \n─────────────────────────────────── column first, right to left ────────────────────────────────────\nAnt Scolopax minor American marten Ursus americanus \nAnteater, australian spiny Anaconda (unidentified) Martes americana American buffalo \nTachyglossus aculeatus Eunectes sp. American racer Bison bison \nAnteater, giant Andean goose Coluber constrictor American crow \nMyrmecophaga tridactyla Chloephaga melanoptera American woodcock Corvus brachyrhynchos\n────────────────────────────────────── equal columns, expand ───────────────────────────────────────\nChloephaga melanoptera American racer Ursus americanus \nAnt Coluber constrictor American buffalo \nAnteater, australian spiny American woodcock Bison bison \nTachyglossus aculeatus Scolopax minor American crow \nAnteater, giant Anaconda (unidentified) Corvus brachyrhynchos \nMyrmecophaga tridactyla Eunectes sp. American marten \n Andean goose Martes americana \n─────────────────────────────────────────── fixed width ────────────────────────────────────────────\nTachyglossus Chloephaga Anaconda Coluber Corvus Ursus americanus\naculeatus melanoptera (unidentified) constrictor brachyrhynchos \nAnteater, giant Ant Eunectes sp. American American marten American buffalo\n woodcock \nMyrmecophaga Anteater, Andean goose Scolopax minor Martes americana Bison bison \ntridactyla australian spiny \n American racer American crow \n\n" + expected = "────────────────────────────────────────────── empty ───────────────────────────────────────────────\n───────────────────────────────────────────── optimal ──────────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n───────────────────────────────────────── optimal, expand ──────────────────────────────────────────\nUrsus americanus American buffalo Bison bison American crow \nCorvus brachyrhynchos American marten Martes americana American racer \nColuber constrictor American woodcock Scolopax minor Anaconda (unidentified)\nEunectes sp. Andean goose Chloephaga melanoptera Ant \nAnteater, australian spiny Tachyglossus aculeatus Anteater, giant Myrmecophaga tridactyla\n────────────────────────────────────── columm first, optimal ───────────────────────────────────────\nUrsus americanus American marten Scolopax minor Ant \nAmerican buffalo Martes americana Anaconda (unidentified) Anteater, australian spiny\nBison bison American racer Eunectes sp. Tachyglossus aculeatus \nAmerican crow Coluber constrictor Andean goose Anteater, giant \nCorvus brachyrhynchos American woodcock Chloephaga melanoptera Myrmecophaga tridactyla \n─────────────────────────────────── column first, right to left ────────────────────────────────────\nAnt Scolopax minor American marten Ursus americanus \nAnteater, australian spiny Anaconda (unidentified) Martes americana American buffalo \nTachyglossus aculeatus Eunectes sp. American racer Bison bison \nAnteater, giant Andean goose Coluber constrictor American crow \nMyrmecophaga tridactyla Chloephaga melanoptera American woodcock Corvus brachyrhynchos\n────────────────────────────────────── equal columns, expand ───────────────────────────────────────\nChloephaga melanoptera American racer Ursus americanus \nAnt Coluber constrictor American buffalo \nAnteater, australian spiny American woodcock Bison bison \nTachyglossus aculeatus Scolopax minor American crow \nAnteater, giant Anaconda (unidentified) Corvus brachyrhynchos \nMyrmecophaga tridactyla Eunectes sp. American marten \n Andean goose Martes americana \n─────────────────────────────────────────── fixed width ────────────────────────────────────────────\nTachyglossus Chloephaga Anaconda Coluber Corvus Ursus american\naculeatus melanoptera (unidentified) constrictor brachyrhynchos \nAnteater, giant Ant Eunectes sp. American American marten American buffa\n woodcock \nMyrmecophaga Anteater, Andean goose Scolopax minor Martes americana Bison bison \ntridactyla australian spiny \n American racer American crow \n\n" assert render() == expected diff --git a/tests/test_table.py b/tests/test_table.py index d54c6a0a..63f90785 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -79,7 +79,7 @@ def render_tables(): 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 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" + 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