This commit is contained in:
Will McGugan 2021-02-24 21:28:34 +00:00
parent 1c1d72ccad
commit 83cc870695
13 changed files with 113 additions and 40 deletions

View File

@ -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

View File

@ -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::

View File

@ -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)

View File

@ -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 <willmcgugan@gmail.com>"]
license = "MIT"

View File

@ -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(
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,

View File

@ -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:
if not self._alt_screen and not self.console.is_jupyter:
self.refresh()
if self.console.is_terminal:
self.console.line()
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)

View File

@ -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 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

View File

@ -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"]:

View File

@ -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:

View File

@ -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

View File

@ -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))

File diff suppressed because one or more lines are too long

View File

@ -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)