From a49e5959969c33f893716eeea57395e8a31ecd93 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 20 Feb 2021 11:05:21 +0000 Subject: [PATCH] fixes --- CHANGELOG.md | 18 ++++++++++++++++++ examples/screen.py | 13 ++++--------- rich/_ratio.py | 5 ++++- rich/console.py | 40 +++++++++++++++++++++++++++------------- rich/layout.py | 23 ++++++++++++++++------- rich/live.py | 6 +++--- rich/markdown.py | 5 ++--- rich/padding.py | 6 ++++++ rich/panel.py | 13 +++++-------- rich/pretty.py | 10 ++++++++-- rich/segment.py | 35 ++++++++++++++++++++++++++++++----- rich/style.py | 1 + rich/syntax.py | 5 ++--- rich/table.py | 2 +- rich/text.py | 37 ++++++++++++++++++++++--------------- tests/test_console.py | 25 +++++++++++++++++++++++-- tests/test_pretty.py | 8 ++++++++ tests/test_ratio.py | 3 +++ tests/test_style.py | 29 ++++++++++++++--------------- tests/test_table.py | 7 ++++++- 20 files changed, 203 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f52c6d..47503e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/screen.py b/examples/screen.py index 90f10288..eb429b19 100644 --- a/examples/screen.py +++ b/examples/screen.py @@ -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", - ) - screen.update(Panel(text)) - sleep(1) + text = Align.center("[blink]Don't Panic![/blink]", vertical="middle") + screen.update(Panel(text)) + sleep(5) diff --git a/rich/_ratio.py b/rich/_ratio.py index f293aba2..bd59943c 100644 --- a/rich/_ratio.py +++ b/rich/_ratio.py @@ -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) diff --git a/rich/console.py b/rich/console.py index a59dbd66..2ff32375 100644 --- a/rich/console.py +++ b/rich/console.py @@ -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'{text}' if rule else text if style.link: text = f'{text}' + text = f'{text}' 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'{text}' + style_number = styles.setdefault(rule, len(styles) + 1) if style.link: - text = f'{text}' + text = f'{text}' + else: + text = f'{text}' append(text) stylesheet_rules: List[str] = [] stylesheet_append = stylesheet_rules.append diff --git a/rich/layout.py b/rich/layout.py index c5284350..ff6ef5b3 100644 --- a/rich/layout.py +++ b/rich/layout.py @@ -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 diff --git a/rich/live.py b/rich/live.py index 696c2c7b..8b23f536 100644 --- a/rich/live.py +++ b/rich/live.py @@ -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: diff --git a/rich/markdown.py b/rich/markdown.py index 761215dd..70256f9d 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -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: diff --git a/rich/padding.py b/rich/padding.py index da308163..860a7c70 100644 --- a/rich/padding.py +++ b/rich/padding.py @@ -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")) \ No newline at end of file diff --git a/rich/panel.py b/rich/panel.py index 2e54d9e6..38888ebd 100644 --- a/rich/panel.py +++ b/rich/panel.py @@ -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) diff --git a/rich/pretty.py b/rich/pretty.py index 8a059928..164b18a1 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -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) diff --git a/rich/segment.py b/rich/segment.py index ff4996aa..c8a2db09 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -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." + ) diff --git a/rich/style.py b/rich/style.py index a6b756b6..594bcd52 100644 --- a/rich/style.py +++ b/rich/style.py @@ -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}") diff --git a/rich/syntax.py b/rich/syntax.py index 7bb749fd..a1cd1f5a 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -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, diff --git a/rich/table.py b/rich/table.py index 41d01160..0336b406 100644 --- a/rich/table.py +++ b/rich/table.py @@ -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") diff --git a/rich/text.py b/rich/text.py index 6a4a332f..0c85bc93 100644 --- a/rich/text.py +++ b/rich/text.py @@ -1114,20 +1114,27 @@ class Text(JupyterMixin): if __name__ == "__main__": # pragma: no cover - from rich import print + from rich.console import Console - text = Text("\n\tHello\n") - 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() \ No newline at end of file diff --git a/tests/test_console.py b/tests/test_console.py index ad1e0f12..187d9b5d 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -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 = '\n\n\n\n\n\n\n \n
foo Click\n
\n
\n\n\n' + expected = '\n\n\n\n\n\n\n \n
foo Click\n
\n
\n\n\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 = '\n\n\n\n\n\n\n \n
foo Click\n
\n
\n\n\n' + print(repr(html)) + expected = '\n\n\n\n\n\n\n \n
foo Click\n
\n
\n\n\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"}) \ No newline at end of file diff --git a/tests/test_pretty.py b/tests/test_pretty.py index 4ba8c279..57fef8dc 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -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()) == "" \ No newline at end of file diff --git a/tests/test_ratio.py b/tests/test_ratio.py index 8a44e8c3..4d7c2fc8 100644 --- a/tests/test_ratio.py +++ b/tests/test_ratio.py @@ -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] diff --git a/tests/test_style.py b/tests/test_style.py index 8282331f..676caf96 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -122,21 +122,20 @@ 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( - reverse=True, - dim=True, - color="red", - bgcolor="blue", - bold=True, - italic=True, - underline=True, - strike=True, - overline=True, - ).get_html_style() - == expected - ) + 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", + bgcolor="blue", + bold=True, + italic=True, + underline=True, + strike=True, + overline=True, + ).get_html_style() + print(repr(html_style)) + assert html_style == expected def test_chain(): diff --git a/tests/test_table.py b/tests/test_table.py index 7a571672..aa1a9ae9 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -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