diff --git a/CHANGELOG.md b/CHANGELOG.md index 352cfc11..a5312c54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ 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). +## [9.12.0] - 2021-02-24 + +### Fixed + +- Fixed issue with Syntax and missing lines in Layout https://github.com/willmcgugan/rich/issues/1050 +- Fixed issue with nested markdown elements https://github.com/willmcgugan/rich/issues/1036 +- Fixed new lines not invoking render hooks https://github.com/willmcgugan/rich/issues/1052 + +### Changed + +- Printing a table with no columns now result in a blank line https://github.com/willmcgugan/rich/issues/1044 + ## [9.11.1] - 2021-02-20 ### Fixed diff --git a/docs/source/tables.rst b/docs/source/tables.rst index ac5e3d4d..9048334c 100644 --- a/docs/source/tables.rst +++ b/docs/source/tables.rst @@ -52,7 +52,18 @@ See :ref:`appendix_box` for other box styles. The :class:`~rich.table.Table` class offers a number of configuration options to set the look and feel of the table, including how borders are rendered and the style and alignment of the columns. -Adding columns +Empty Tables +~~~~~~~~~~~~ + +Printing a table with no columns results in a blank line. If you are building a table dynamically and the data source has no columns, you might want to print something different. Here's how you might do that:: + + if table.columns: + print(table) + else: + print("[i]No data...[/i]") + + +Adding Columns ~~~~~~~~~~~~~~ You may also add columns by specifying them in the positional arguments of the :class:`~rich.table.Table` constructor. For example, we could construct a table with three columns like this:: diff --git a/examples/layout.py b/examples/layout.py index 046476b5..a2ced2e2 100644 --- a/examples/layout.py +++ b/examples/layout.py @@ -49,7 +49,7 @@ class Clock: layout["header"].update(Clock()) -with Live(layout, screen=True) as live: +with Live(layout, screen=True, redirect_stderr=False) as live: try: while True: sleep(1) diff --git a/pyproject.toml b/pyproject.toml index 89f886ea..df40a0b0 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 = "9.11.1" +version = "9.12.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/console.py b/rich/console.py index 2ff32375..78c2e58a 100644 --- a/rich/console.py +++ b/rich/console.py @@ -23,7 +23,6 @@ from typing import ( NamedTuple, Optional, TextIO, - Tuple, Union, cast, ) @@ -32,7 +31,7 @@ from typing_extensions import Literal, Protocol, runtime_checkable from . import errors, themes from ._emoji_replace import _emoji_replace -from ._log_render import LogRender, FormatTimeCallable +from ._log_render import FormatTimeCallable, LogRender from .align import Align, AlignMethod from .color import ColorSystem from .control import Control @@ -148,7 +147,7 @@ class ConsoleOptions: """Update values, return a copy.""" options = replace(self) if not isinstance(width, NoChange): - options.min_width = options.max_width = width + options.min_width = options.max_width = max(0, width) if not isinstance(min_width, NoChange): options.min_width = min_width if not isinstance(max_width, NoChange): @@ -162,7 +161,7 @@ class ConsoleOptions: if not isinstance(highlight, NoChange): options.highlight = highlight if not isinstance(height, NoChange): - options.height = height + options.height = None if height is None else max(0, height) return options def update_width(self, width: int) -> "ConsoleOptions": @@ -174,6 +173,7 @@ class ConsoleOptions: Returns: ~ConsoleOptions: New console options instance """ + width = max(0, width) options = replace(self, min_width=width, max_width=width) return options @@ -210,6 +210,18 @@ class CaptureError(Exception): """An error in the Capture context manager.""" +class NewLine: + """A renderable to generate new line(s)""" + + def __init__(self, count: int = 1) -> None: + self.count = count + + def __rich_console__( + self, console: "Console", options: "ConsoleOptions" + ) -> Iterable[Segment]: + yield Segment("\n" * self.count) + + class Capture: """Context manager to capture the result of printing to the console. See :meth:`~rich.console.Console.capture` for how to use. @@ -913,9 +925,7 @@ class Console: """ assert count >= 0, "count must be >= 0" - if count: - self._buffer.append(Segment("\n" * count)) - self._check_buffer() + self.print(NewLine(count)) def clear(self, home: bool = True) -> None: """Clear the screen. @@ -1072,7 +1082,7 @@ class Console: options (Optional[ConsoleOptions], optional): Console options, or None to use self.options. Default to ``None``. style (Style, optional): Optional style to apply to renderables. Defaults to ``None``. pad (bool, optional): Pad lines shorter than render width. Defaults to ``True``. - new_lines (bool, optional): Include "\n" characters at end of line. + new_lines (bool, optional): Include "\n" characters at end of lines. Returns: List[List[Segment]]: A list of lines, where a line is a list of Segment objects. @@ -1082,17 +1092,27 @@ class Console: if style is not None: _rendered = Segment.apply_style(_rendered, style) lines = list( - Segment.split_and_crop_lines( - _rendered, - render_options.max_width, - include_new_lines=new_lines, - pad=pad, + islice( + Segment.split_and_crop_lines( + _rendered, + render_options.max_width, + include_new_lines=new_lines, + pad=pad, + ), + None, + render_options.height, ) ) if render_options.height is not None: - lines = Segment.set_shape( - lines, render_options.max_width, render_options.height, style=style - ) + extra_lines = render_options.height - len(lines) + if extra_lines > 0: + pad_line = [ + [Segment(" " * render_options.max_width, style), Segment("\n")] + if new_lines + else [Segment(" " * render_options.max_width, style)] + ] + lines.extend(pad_line * extra_lines) + return lines def render_str( @@ -1357,8 +1377,7 @@ class Console: Console default. Defaults to ``None``. """ if not objects: - self.line() - return + objects = (NewLine(),) if soft_wrap is None: soft_wrap = self.soft_wrap @@ -1470,8 +1489,8 @@ class Console: _stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1. """ if not objects: - self.line() - return + objects = (NewLine(),) + with self: renderables = self._collect_renderables( objects, diff --git a/rich/live.py b/rich/live.py index 8b23f536..41732501 100644 --- a/rich/live.py +++ b/rich/live.py @@ -130,14 +130,14 @@ class Live(JupyterMixin, RenderHook): self._refresh_thread.stop() # allow it to fully render on the last even if overflow self.vertical_overflow = "visible" - if not self._alt_screen: - if not self.console.is_jupyter: - self.refresh() - if self.console.is_terminal: - self.console.line() + if not self._alt_screen and not self.console.is_jupyter: + self.refresh() + finally: self._disable_redirect_io() self.console.pop_render_hook() + if not self._alt_screen and self.console.is_terminal: + self.console.line() self.console.show_cursor(True) if self._alt_screen: self.console.set_alt_screen(False) diff --git a/rich/markdown.py b/rich/markdown.py index 70256f9d..23c6124d 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -367,7 +367,7 @@ class MarkdownContext: def on_text(self, text: str, node_type: str) -> None: """Called when the parser visits text.""" - if node_type == "code" and self._syntax is not None: + if node_type in "code" and self._syntax is not None: highlight_text = self._syntax.highlight(text) highlight_text.rstrip() self.stack.top.on_text( @@ -517,10 +517,13 @@ class Markdown(JupyterMixin): if current.literal: element.on_text(context, current.literal.rstrip()) context.stack.pop() - if new_line: - yield Segment("\n") - yield from console.render(element, context.options) - element.on_leave(context) + if context.stack.top.on_child_close(context, element): + if new_line: + yield Segment("\n") + yield from console.render(element, context.options) + element.on_leave(context) + else: + element.on_leave(context) new_line = element.new_line diff --git a/rich/segment.py b/rich/segment.py index c8a2db09..f2c2a8c5 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -275,6 +275,7 @@ class Segment(NamedTuple): width: int, height: int = None, style: Style = None, + new_lines: bool = False, ) -> List[List["Segment"]]: """Set the shape of a list of lines (enclosing rectangle). @@ -283,15 +284,21 @@ class Segment(NamedTuple): width (int): Desired width. height (int, optional): Desired height or None for no change. style (Style, optional): Style of any padding added. Defaults to None. + new_lines (bool, optional): Padded lines should include "\n". Defaults to False. Returns: List[List[Segment]]: New list of lines that fits width x height. """ if height is None: height = len(lines) - new_lines: List[List[Segment]] = [] - pad_line = [Segment(" " * width, style)] - append = new_lines.append + shaped_lines: List[List[Segment]] = [] + pad_line = ( + [Segment(" " * width, style), Segment("\n")] + if new_lines + else [Segment(" " * width, style)] + ) + + append = shaped_lines.append adjust_line_length = cls.adjust_line_length line: Optional[List[Segment]] iter_lines = iter(lines) @@ -301,7 +308,7 @@ class Segment(NamedTuple): append(pad_line) else: append(adjust_line_length(line, width, style=style)) - return new_lines + return shaped_lines @classmethod def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]: diff --git a/rich/syntax.py b/rich/syntax.py index a1cd1f5a..562674f9 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -530,10 +530,11 @@ class Syntax(JupyterMixin): if self.word_wrap: wrapped_lines = console.render_lines( line, - render_options, + render_options.update(height=None), style=background_style, pad=not transparent_background, ) + else: segments = list(line.render(console, end="")) if options.no_wrap: diff --git a/rich/table.py b/rich/table.py index 0336b406..e8757597 100644 --- a/rich/table.py +++ b/rich/table.py @@ -410,6 +410,10 @@ class Table(JupyterMixin): self, console: "Console", options: "ConsoleOptions" ) -> "RenderResult": + if not self.columns: + yield Segment("\n") + return + max_width = options.max_width if self.width is not None: max_width = self.width diff --git a/tests/test_log.py b/tests/test_log.py index 82dbfe28..9f8a22a4 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -38,7 +38,7 @@ def render_log(): def test_log(): expected = replace_link_ids( - "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:34\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:35\x1b[0m\n \x1b[34m╭─\x1b[0m\x1b[34m───────────────────── \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m ─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \n \x1b[34m│\x1b[0m \x1b[3;33mconsole\x1b[0m\x1b[31m =\x1b[0m \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m \x1b[34m│\x1b[0m \n \x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \n" + "\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:33\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:34\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:35\x1b[0m\n \x1b[34m╭─\x1b[0m\x1b[34m───────────────────── \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m ─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \n \x1b[34m│\x1b[0m \x1b[3;33mconsole\x1b[0m\x1b[31m =\x1b[0m \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m \x1b[34m│\x1b[0m \n \x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \n" ) rendered = render_log() print(repr(rendered)) diff --git a/tests/test_markdown.py b/tests/test_markdown.py index fdd56b72..4602131b 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -61,6 +61,12 @@ import this foobar ``` + import this + + +1. List item + + Code block """ import io @@ -87,13 +93,14 @@ def render(renderable: RenderableType) -> str: ) console.print(renderable) output = replace_link_ids(console.file.getvalue()) + print(repr(output)) return output def test_markdown_render(): markdown = Markdown(MARKDOWN) rendered_markdown = render(markdown) - expected = "╔══════════════════════════════════════════════════════════════════════════════════════════════════╗\n║ \x1b[1mHeading\x1b[0m ║\n╚══════════════════════════════════════════════════════════════════════════════════════════════════╝\n\n\n \x1b[1;4mSub-heading\x1b[0m \n\n \x1b[1mHeading\x1b[0m \n\n \x1b[1;2mH4 Heading\x1b[0m \n\n \x1b[4mH5 Heading\x1b[0m \n\n \x1b[3mH6 Heading\x1b[0m \n\nParagraphs are separated by a blank line. \n\nTwo spaces at the end of a line \nproduces a line break. \n\nText attributes \x1b[3mitalic\x1b[0m, \x1b[1mbold\x1b[0m, \x1b[97;40mmonospace\x1b[0m. \n\nHorizontal rule: \n\n\x1b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\nBullet list: \n\n\x1b[1;33m • \x1b[0mapples \n\x1b[1;33m • \x1b[0moranges \n\x1b[1;33m • \x1b[0mpears \n\nNumbered list: \n\n\x1b[1;33m 1 \x1b[0mlather \n\x1b[1;33m 2 \x1b[0mrinse \n\x1b[1;33m 3 \x1b[0mrepeat \n\nAn \x1b]8;id=0;foo\x1b\\\x1b[94mexample\x1b[0m\x1b]8;;\x1b\\. \n\n\x1b[35m▌ \x1b[0m\x1b[35mMarkdown uses email-style > characters for blockquoting.\x1b[0m\x1b[35m \x1b[0m\n\x1b[35m▌ \x1b[0m\x1b[35mLorem ipsum\x1b[0m\x1b[35m \x1b[0m\n\n🌆 \x1b]8;id=0;foo\x1b\\progress\x1b]8;;\x1b\\ \n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34ma=1 \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[38;2;249;38;114;48;2;39;40;34mimport\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mthis\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mfoobar \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n" + expected = "╔══════════════════════════════════════════════════════════════════════════════════════════════════╗\n║ \x1b[1mHeading\x1b[0m ║\n╚══════════════════════════════════════════════════════════════════════════════════════════════════╝\n\n\n \x1b[1;4mSub-heading\x1b[0m \n\n \x1b[1mHeading\x1b[0m \n\n \x1b[1;2mH4 Heading\x1b[0m \n\n \x1b[4mH5 Heading\x1b[0m \n\n \x1b[3mH6 Heading\x1b[0m \n\nParagraphs are separated by a blank line. \n\nTwo spaces at the end of a line \nproduces a line break. \n\nText attributes \x1b[3mitalic\x1b[0m, \x1b[1mbold\x1b[0m, \x1b[97;40mmonospace\x1b[0m. \n\nHorizontal rule: \n\n\x1b[33m────────────────────────────────────────────────────────────────────────────────────────────────────\x1b[0m\nBullet list: \n\n\x1b[1;33m • \x1b[0mapples \n\x1b[1;33m • \x1b[0moranges \n\x1b[1;33m • \x1b[0mpears \n\nNumbered list: \n\n\x1b[1;33m 1 \x1b[0mlather \n\x1b[1;33m 2 \x1b[0mrinse \n\x1b[1;33m 3 \x1b[0mrepeat \n\nAn \x1b]8;id=0;foo\x1b\\\x1b[94mexample\x1b[0m\x1b]8;;\x1b\\. \n\n\x1b[35m▌ \x1b[0m\x1b[35mMarkdown uses email-style > characters for blockquoting.\x1b[0m\x1b[35m \x1b[0m\n\x1b[35m▌ \x1b[0m\x1b[35mLorem ipsum\x1b[0m\x1b[35m \x1b[0m\n\n🌆 \x1b]8;id=0;foo\x1b\\progress\x1b]8;;\x1b\\ \n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34ma=1 \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[38;2;249;38;114;48;2;39;40;34mimport\x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34m \x1b[0m\x1b[38;2;248;248;242;48;2;39;40;34mthis\x1b[0m\x1b[48;2;39;40;34m \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mfoobar \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[2m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mimport this \x1b[0m \x1b[2m│\x1b[0m\n\x1b[2m└──────────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n\n\x1b[1;33m 1 \x1b[0mList item \n\x1b[1;33m \x1b[0m\x1b[2m┌───────────────────────────────────────────────────────────────────────────────────────────────┐\x1b[0m\n\x1b[1;33m \x1b[0m\x1b[2m│\x1b[0m \x1b[48;2;39;40;34mCode block \x1b[0m \x1b[2m│\x1b[0m\n\x1b[1;33m \x1b[0m\x1b[2m└───────────────────────────────────────────────────────────────────────────────────────────────┘\x1b[0m\n" assert rendered_markdown == expected diff --git a/tests/test_table.py b/tests/test_table.py index aa1a9ae9..1b3df42c 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -141,6 +141,15 @@ def test_min_width(): assert all(len(line) == 30 for line in output.splitlines()) +def test_no_columns(): + console = Console(color_system=None) + console.begin_capture() + console.print(Table()) + output = console.end_capture() + print(repr(output)) + assert output == "\n" + + if __name__ == "__main__": render = render_tables() print(render)