This commit is contained in:
Will McGugan 2021-02-20 11:05:21 +00:00
parent d359770970
commit a49e595996
20 changed files with 203 additions and 88 deletions

View File

@ -5,6 +5,24 @@ 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.11.1] - Unreleased
### Fixed
- Fixed table with expand=False not expanding when justify="center"
- Fixed single renderable in Layout not respecting height
- Fixed COLUMNS and LINES env var https://github.com/willmcgugan/rich/issues/1019
- Layout now respects minimum_size when fixes sizes are greater than available space
- HTML export now changes link underline score to match terminal https://github.com/willmcgugan/rich/issues/1009
### Changed
- python -m rich.markdown and rich.syntax show usage with no file
### Added
- Added height parameter to Layout
## [9.11.0] - 2021-02-15
### Fixed

View File

@ -4,18 +4,13 @@ Demonstration of Console.screen()
from time import sleep
from rich.console import Console
from rich.align import Align
from rich.text import Text
from rich.console import Console
from rich.panel import Panel
console = Console()
with console.screen(style="bold white on red") as screen:
for count in range(5, 0, -1):
text = Align.center(
Text.from_markup(f"[blink]Don't Panic![/blink]\n{count}", justify="center"),
vertical="middle",
)
text = Align.center("[blink]Don't Panic![/blink]", vertical="middle")
screen.update(Panel(text))
sleep(1)
sleep(5)

View File

@ -42,7 +42,10 @@ def ratio_resolve(total: int, edges: Sequence[Edge]) -> List[int]:
remaining = total - sum(size or 0 for size in sizes)
if remaining <= 0:
# No room for flexible edges
return [(size or 1) for size in sizes]
return [
((edge.minimum_size or 1) if size is None else size)
for size, edge in zip(sizes, edges)
]
# Calculate number of characters in a ratio portion
portion = remaining / sum((edge.ratio or 1) for _, edge in flexible_edges)

View File

@ -299,9 +299,7 @@ class ScreenContext:
self.screen = Screen(style=style)
self._changed = False
def update(
self, renderable: RenderableType = None, style: StyleType = None
) -> None:
def update(self, *renderables: RenderableType, style: StyleType = None) -> None:
"""Update the screen.
Args:
@ -309,8 +307,10 @@ class ScreenContext:
or None for no change. Defaults to None.
style: (Style, optional): Replacement style, or None for no change. Defaults to None.
"""
if renderable is not None:
self.screen.renderable = renderable
if renderables:
self.screen.renderable = (
RenderGroup(*renderables) if len(renderables) > 1 else renderables[0]
)
if style is not None:
self.screen.style = style
self.console.print(self.screen, end="")
@ -532,6 +532,16 @@ class Console:
if self.is_jupyter:
width = width or 93
height = height or 100
if width is None:
columns = self._environ.get("COLUMNS")
if columns is not None and columns.isdigit():
width = int(columns)
if height is None:
lines = self._environ.get("LINES")
if lines is not None and lines.isdigit():
height = int(lines)
self.soft_wrap = soft_wrap
self._width = width
self._height = height
@ -1050,6 +1060,7 @@ class Console:
*,
style: Optional[Style] = None,
pad: bool = True,
new_lines: bool = False,
) -> List[List[Segment]]:
"""Render objects in to a list of lines.
@ -1061,7 +1072,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``.
range (Optional[Tuple[int, int]], optional): Range of lines to render, or ``None`` for all line. Defaults to ``None``
new_lines (bool, optional): Include "\n" characters at end of line.
Returns:
List[List[Segment]]: A list of lines, where a line is a list of Segment objects.
@ -1072,7 +1083,10 @@ class Console:
_rendered = Segment.apply_style(_rendered, style)
lines = list(
Segment.split_and_crop_lines(
_rendered, render_options.max_width, include_new_lines=False, pad=pad
_rendered,
render_options.max_width,
include_new_lines=new_lines,
pad=pad,
)
)
if render_options.height is not None:
@ -1368,7 +1382,7 @@ class Console:
for hook in self._render_hooks:
renderables = hook.process_renderables(renderables)
render_options = self.options.update(
justify="default",
justify=justify,
overflow=overflow,
width=min(width, self.width) if width else NO_CHANGE,
height=height,
@ -1697,9 +1711,9 @@ class Console:
text = escape(text)
if style:
rule = style.get_html_style(_theme)
text = f'<span style="{rule}">{text}</span>' if rule else text
if style.link:
text = f'<a href="{style.link}">{text}</a>'
text = f'<span style="{rule}">{text}</span>' if rule else text
append(text)
else:
styles: Dict[str, int] = {}
@ -1709,11 +1723,11 @@ class Console:
text = escape(text)
if style:
rule = style.get_html_style(_theme)
if rule:
style_number = styles.setdefault(rule, len(styles) + 1)
text = f'<span class="r{style_number}">{text}</span>'
if style.link:
text = f'<a href="{style.link}">{text}</a>'
text = f'<a class="r{style_number}" href="{style.link}">{text}</a>'
else:
text = f'<span class="r{style_number}">{text}</span>'
append(text)
stylesheet_rules: List[str] = []
stylesheet_append = stylesheet_rules.append

View File

@ -1,6 +1,7 @@
from .align import Align
from .console import Console, ConsoleOptions, RenderResult, RenderableType
from .highlighter import ReprHighlighter
from ._loop import loop_last
from .panel import Panel
from .pretty import Pretty
from ._ratio import ratio_resolve
@ -77,6 +78,7 @@ class Layout:
ratio: int = 1,
name: str = None,
visible: bool = True,
height: int = None,
) -> None:
self._renderable = renderable or _Placeholder(self)
self.direction = direction
@ -85,6 +87,7 @@ class Layout:
self.ratio = ratio
self.name = name
self.visible = visible
self.height = height
self._children: List[Layout] = []
def __repr__(self) -> str:
@ -179,13 +182,19 @@ class Layout:
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
options = options.update(height=options.height or options.size.height)
render_options = options.update(
height=options.height or self.height or options.size.height
)
if not self.children:
yield from console.render(self._renderable or "", options)
for line in console.render_lines(
self._renderable or "", render_options, new_lines=True
):
yield from line
elif self.direction == "vertical":
yield from self._render_vertical(console, options)
yield from self._render_vertical(console, render_options)
elif self.direction == "horizontal":
yield from self._render_horizontal(console, options)
yield from self._render_horizontal(console, render_options)
def _render_horizontal(
self, console: Console, options: ConsoleOptions
@ -206,14 +215,14 @@ class Layout:
) -> RenderResult:
render_heights = ratio_resolve(options.height or console.height, self.children)
renders = [
console.render_lines(child.renderable, options.update(height=render_height))
console.render_lines(
child.renderable, options.update(height=render_height), new_lines=True
)
for child, render_height in zip(self.children, render_heights)
]
new_line = Segment.line()
for render in renders:
for line in render:
yield from line
yield new_line
if __name__ == "__main__": # type: ignore

View File

@ -40,7 +40,7 @@ class Live(JupyterMixin, RenderHook):
screen (bool, optional): Enable alternate screen mode. Defaults to False.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 1.
transient (bool, optional): Clear the renderable on exit. Defaults to False.
transient (bool, optional): Clear the renderable on exit (has no effect when screen=True). Defaults to False.
redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
@ -76,7 +76,7 @@ class Live(JupyterMixin, RenderHook):
self.ipy_widget: Optional[Any] = None
self.auto_refresh = auto_refresh
self._started: bool = False
self.transient = transient
self.transient = True if screen else transient
self._refresh_thread: Optional[_RefreshThread] = None
self.refresh_per_second = refresh_per_second
@ -145,7 +145,7 @@ class Live(JupyterMixin, RenderHook):
if self._refresh_thread is not None:
self._refresh_thread.join()
self._refresh_thread = None
if self.transient and not self._screen:
if self.transient and not self._alt_screen:
self.console.control(self._live_render.restore_cursor())
if self.ipy_widget is not None: # pragma: no cover
if self.transient:

View File

@ -535,8 +535,7 @@ if __name__ == "__main__": # pragma: no cover
parser.add_argument(
"path",
metavar="PATH",
nargs="?",
help="path to markdown file",
help="path to markdown file, or - for stdin",
)
parser.add_argument(
"-c",
@ -593,7 +592,7 @@ if __name__ == "__main__": # pragma: no cover
from rich.console import Console
if not args.path or args.path == "-":
if args.path == "-":
markdown_body = sys.stdin.read()
else:
with open(args.path, "rt", encoding="utf-8") as markdown_file:

View File

@ -122,3 +122,9 @@ class Padding(JupyterMixin):
measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
measurement = measurement.with_maximum(max_width)
return measurement
if __name__ == "__main__": # pragma: no cover
from rich import print
print(Padding("Hello, World", (2, 4), style="on blue"))

View File

@ -192,15 +192,12 @@ if __name__ == "__main__": # pragma: no cover
from .box import ROUNDED, DOUBLE
p = Panel(
Panel.fit(
Text.from_markup("[bold magenta]Hello World!"),
box=ROUNDED,
safe_box=True,
style="on red",
),
title="[b]Hello, World",
"Hello, World!",
title="rich.Panel",
style="white on blue",
box=DOUBLE,
padding=1,
)
print(p)
c.print()
c.print(p)

View File

@ -192,7 +192,11 @@ class Pretty:
no_wrap=pick_bool(self.no_wrap, options.no_wrap),
style="pretty",
)
pretty_text = self.highlighter(pretty_text)
pretty_text = (
self.highlighter(pretty_text)
if pretty_text
else Text("__repr__ return empty string", style="dim italic")
)
if self.indent_guides and not options.ascii_only:
pretty_text = pretty_text.with_indent_guides(
self.indent_size, style="repr.indent"
@ -209,7 +213,9 @@ class Pretty:
max_length=self.max_length,
max_string=self.max_string,
)
text_width = max(cell_len(line) for line in pretty_str.splitlines())
text_width = (
max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0
)
return Measurement(text_width, text_width)

View File

@ -384,9 +384,34 @@ class Segment(NamedTuple):
if __name__ == "__main__": # pragma: no cover
lines = [[Segment("Hello")]]
lines = Segment.set_shape(lines, 50, 4, style=Style.parse("on blue"))
for line in lines:
print(line)
from rich.syntax import Syntax
from rich.text import Text
from rich.console import Console
print(Style.parse("on blue") + Style.parse("on red"))
code = """from rich.console import Console
console = Console()
text = Text.from_markup("Hello, [bold magenta]World[/]!")
console.print(text)"""
text = Text.from_markup("Hello, [bold magenta]World[/]!")
console = Console()
console.rule("rich.Segment")
console.print(
"A Segment is the last step in the Rich render process before gemerating text with ANSI codes."
)
console.print("\nConsider the following code:\n")
console.print(Syntax(code, "python", line_numbers=True))
console.print()
console.print(
"When you call [b]print()[/b], Rich [i]renders[/i] the object in to the the following:\n"
)
fragments = list(console.render(text))
console.print(fragments)
console.print()
console.print("The Segments are then processed to produce the following output:\n")
console.print(text)
console.print(
"\nYou will only need to know this if you are implementing your own Rich renderables."
)

View File

@ -512,6 +512,7 @@ class Style:
if color is not None:
theme_color = color.get_truecolor(theme)
append(f"color: {theme_color.hex}")
append(f"text-decoration-color: {theme_color.hex}")
if bgcolor is not None:
theme_color = bgcolor.get_truecolor(theme, foreground=False)
append(f"background-color: {theme_color.hex}")

View File

@ -573,8 +573,7 @@ if __name__ == "__main__": # pragma: no cover
parser.add_argument(
"path",
metavar="PATH",
nargs="?",
help="path to file",
help="path to file, or - for stdin",
)
parser.add_argument(
"-c",
@ -646,7 +645,7 @@ if __name__ == "__main__": # pragma: no cover
console = Console(force_terminal=args.force_color, width=args.width)
if not args.path or args.path == "-":
if args.path == "-":
code = sys.stdin.read()
syntax = Syntax(
code=code,

View File

@ -851,7 +851,7 @@ if __name__ == "__main__": # pragma: no cover
table.expand = True
header("expand=True")
console.print(table, justify="center")
console.print(table)
table.width = 50
header("width=50")

View File

@ -1114,20 +1114,27 @@ class Text(JupyterMixin):
if __name__ == "__main__": # pragma: no cover
from rich import print
from rich.console import Console
text = Text("<span>\n\tHello\n</span>")
text.expand_tabs(4)
print(text)
text = Text(
"""\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"""
)
text.highlight_words(["Lorem"], "bold")
text.highlight_words(["ipsum"], "italic")
code = """
def __add__(self, other: Any) -> "Text":
if isinstance(other, (str, Text)):
result = self.copy()
result.append(other)
return result
return NotImplemented
"""
text = Text(code)
text = text.with_indent_guides()
print(text)
console = Console()
console.rule("justify='left'")
console.print(text, style="red")
console.print()
console.rule("justify='center'")
console.print(text, style="green", justify="center")
console.print()
console.rule("justify='right'")
console.print(text, style="blue", justify="right")
console.print()
console.rule("justify='full'")
console.print(text, style="magenta", justify="full")
console.print()

View File

@ -349,7 +349,7 @@ def test_export_html():
console = Console(record=True, width=100)
console.print("[b]foo [link=https://example.org]Click[/link]")
html = console.export_html()
expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n.r1 {font-weight: bold}\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span class="r1">foo </span><a href="https://example.org"><span class="r1">Click</span></a>\n</pre>\n </code>\n</body>\n</html>\n'
expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n.r1 {font-weight: bold}\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span class="r1">foo </span><a class="r1" href="https://example.org">Click</a>\n</pre>\n </code>\n</body>\n</html>\n'
assert html == expected
@ -357,7 +357,8 @@ def test_export_html_inline():
console = Console(record=True, width=100)
console.print("[b]foo [link=https://example.org]Click[/link]")
html = console.export_html(inline_styles=True)
expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="font-weight: bold">foo </span><a href="https://example.org"><span style="font-weight: bold">Click</span></a>\n</pre>\n </code>\n</body>\n</html>\n'
print(repr(html))
expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n color: #000000;\n background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n <code>\n <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="font-weight: bold">foo </span><span style="font-weight: bold"><a href="https://example.org">Click</a></span>\n</pre>\n </code>\n</body>\n</html>\n'
assert html == expected
@ -543,3 +544,23 @@ def test_screen_update():
def test_height():
console = Console(width=80, height=46)
assert console.height == 46
def test_columns_env():
console = Console(_environ={"COLUMNS": "314"})
assert console.width == 314
# width take precedence
console = Console(width=40, _environ={"COLUMNS": "314"})
assert console.width == 40
# Should not fail
console = Console(width=40, _environ={"COLUMNS": "broken"})
def test_lines_env():
console = Console(_environ={"LINES": "220"})
assert console.height == 220
# height take precedence
console = Console(height=40, _environ={"LINES": "220"})
assert console.height == 40
# Should not fail
console = Console(width=40, _environ={"LINES": "broken"})

View File

@ -143,3 +143,11 @@ def test_newline():
result = console.end_capture()
expected = "\n(\n 1,\n)\n"
assert result == expected
def test_empty_repr():
class Foo:
def __repr__(self):
return ""
assert pretty_repr(Foo()) == ""

View File

@ -48,3 +48,6 @@ def test_ratio_resolve():
33,
34,
]
assert ratio_resolve(
50, [Edge(size=30), Edge(ratio=1, minimum_size=10), Edge(size=30)]
) == [30, 10, 30]

View File

@ -122,9 +122,8 @@ def test_link_id():
def test_get_html_style():
expected = "color: #7f7fbf; background-color: #800000; font-weight: bold; font-style: italic; text-decoration: underline; text-decoration: line-through; text-decoration: overline"
assert (
Style(
expected = "color: #7f7fbf; text-decoration-color: #7f7fbf; background-color: #800000; font-weight: bold; font-style: italic; text-decoration: underline; text-decoration: line-through; text-decoration: overline"
html_style = Style(
reverse=True,
dim=True,
color="red",
@ -135,8 +134,8 @@ def test_get_html_style():
strike=True,
overline=True,
).get_html_style()
== expected
)
print(repr(html_style))
assert html_style == expected
def test_chain():

View File

@ -21,13 +21,15 @@ def render_tables():
color_system=None,
)
table = Table(title="test table", caption="table caption", expand=True)
table = Table(title="test table", caption="table caption", expand=False)
table.add_column("foo", footer=Text("total"), no_wrap=True, overflow="ellipsis")
table.add_column("bar", justify="center")
table.add_column("baz", justify="right")
table.add_row("Averlongwordgoeshere", "banana pancakes", None)
assert Measurement.get(console, table, 80) == Measurement(41, 48)
table.expand = True
assert Measurement.get(console, table, 80) == Measurement(41, 48)
for width in range(10, 60, 5):
@ -63,6 +65,9 @@ def render_tables():
table.width = 20
assert Measurement.get(console, table, 80) == Measurement(20, 20)
table.expand = False
assert Measurement.get(console, table, 80) == Measurement(20, 20)
table.expand = True
console.print(table)
table.columns[0].no_wrap = True