rich measure

This commit is contained in:
Will McGugan 2021-03-25 21:08:36 +00:00
parent 27379a8f1a
commit be5e9dca2d
43 changed files with 301 additions and 185 deletions

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed Layout.split to use new Splitter class - Changed Layout.split to use new Splitter class
- Improved layout.tree - Improved layout.tree
- Changed default theme color for repr.number to cyan - Changed default theme color for repr.number to cyan
- `__rich_measure__` signature changed to accept ConsoleOptions rather than max_width
### Added ### Added
@ -29,6 +30,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added Layout.add_split, Layout.split_column, Layout.split_row, layout.refresh - Added Layout.add_split, Layout.split_column, Layout.split_row, layout.refresh
- Added new Rich repr protocol `__rich_repr__` - Added new Rich repr protocol `__rich_repr__`
### Fixed
- Fixed table style taking precedence over row style https://github.com/willmcgugan/rich/issues/1129
- Fixed incorrect measurement of Text with new lines and whitespace https://github.com/willmcgugan/rich/issues/1133
## [9.13.0] - 2021-03-06 ## [9.13.0] - 2021-03-06
### Added ### Added

View file

@ -18,25 +18,22 @@ To define a layout, construct a Layout object and print it::
layout = Layout() layout = Layout()
print(layout) print(layout)
This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, let's create a more interesting layout by calling the :meth:`~rich.layout.Layout.split` method to divide the layout in to two sub-layouts:: This will draw a box the size of the terminal with some information regarding the layout. The box is a "placeholder" because we have yet to add any content to it. Before we do that, let's create a more interesting layout by calling the :meth:`~rich.layout.Layout.split_column` method to divide the layout in to two sub-layouts::
layout.split( layout.split_column(
Layout(name="upper"), Layout(name="upper"),
Layout(name="lower") Layout(name="lower")
) )
print(layout) print(layout)
This will divide the terminal screen in to two equal sized portions, one on top of the other. The ``name`` attribute is an internal identifier we can use to look up the sub-layout later. Let's use that to create another split:: This will divide the terminal screen in to two equal sized portions, one on top of the other. The ``name`` attribute is an internal identifier we can use to look up the sub-layout later. Let's use that to create another split, this time we will call :meth:`~rich.layout.Layout.split_row` to split the lower layout in to a row of two sub-layouts.
layout["lower"].split( layout["lower"].split_row(
Layout(name="left"), Layout(name="left"),
Layout(name="right"), Layout(name="right"),
direction="horizontal"
) )
print(layout) print(layout)
The addition of the ``direction="horizontal"`` tells the Layout class to split left-to-right, rather than the default of top-to-bottom.
You should now see the screen area divided in to 3 portions; an upper half and a lower half that is split in to two quarters. You should now see the screen area divided in to 3 portions; an upper half and a lower half that is split in to two quarters.
.. raw:: html .. raw:: html

View file

@ -64,10 +64,10 @@ For complete control over how a custom object is rendered to the terminal, you c
Measuring Renderables Measuring Renderables
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class, for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the renderable objects in the Rich module, you will need to supply a ``__rich_measure__`` method which accepts a :class:`~rich.console.Console` and the maximum width and returns a :class:`~rich.measure.Measurement` object. The Measurement object should contain the *minimum* and *maximum* number of characters required to render. Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class, for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the renderable objects in the Rich module, you will need to supply a ``__rich_measure__`` method which accepts a :class:`~rich.console.Console` and :class:`~rich.console.ConsoleOptions` and returns a :class:`~rich.measure.Measurement` object. The Measurement object should contain the *minimum* and *maximum* number of characters required to render.
For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board):: For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board)::
class ChessBoard: class ChessBoard:
def __rich_measure__(self, console: Console, max_width: int) -> Measurement: def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
return Measurement(8, max_width) return Measurement(8, options.max_width)

View file

@ -2,7 +2,7 @@
name = "rich" name = "rich"
homepage = "https://github.com/willmcgugan/rich" homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/" documentation = "https://rich.readthedocs.io/en/latest/"
version = "9.13.0" version = "10.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"] authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT" license = "MIT"

View file

@ -30,8 +30,10 @@ class ColorBox:
yield Segment("", Style(color=color, bgcolor=bgcolor)) yield Segment("", Style(color=color, bgcolor=bgcolor))
yield Segment.line() yield Segment.line()
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
return Measurement(1, max_width) self, console: "Console", options: ConsoleOptions
) -> Measurement:
return Measurement(1, options.max_width)
def make_test_card() -> Table: def make_test_card() -> Table:

View file

@ -133,7 +133,7 @@ class Align(JupyterMixin):
self, console: "Console", options: "ConsoleOptions" self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult": ) -> "RenderResult":
align = self.align align = self.align
width = Measurement.get(console, self.renderable).maximum width = Measurement.get(console, options, self.renderable).maximum
rendered = console.render( rendered = console.render(
Constrain( Constrain(
self.renderable, width if self.width is None else min(width, self.width) self.renderable, width if self.width is None else min(width, self.width)
@ -221,8 +221,10 @@ class Align(JupyterMixin):
iter_segments = Segment.apply_style(iter_segments, style) iter_segments = Segment.apply_style(iter_segments, style)
yield from iter_segments yield from iter_segments
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
measurement = Measurement.get(console, self.renderable, max_width) self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement return measurement
@ -275,8 +277,10 @@ class VerticalCenter(JupyterMixin):
if bottom_space > 0: if bottom_space > 0:
yield from blank_lines(bottom_space) yield from blank_lines(bottom_space)
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
measurement = Measurement.get(console, self.renderable, max_width) self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
measurement = Measurement.get(console, options, self.renderable)
return measurement return measurement

View file

@ -49,7 +49,10 @@ class Bar(JupyterMixin):
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
width = min(self.width or options.max_width, options.max_width) width = min(
self.width if self.width is not None else options.max_width,
options.max_width,
)
if self.begin >= self.end: if self.begin >= self.end:
yield Segment(" " * width, self.style) yield Segment(" " * width, self.style)
@ -81,9 +84,11 @@ class Bar(JupyterMixin):
yield Segment(prefix + body[len(prefix) :] + suffix, self.style) yield Segment(prefix + body[len(prefix) :] + suffix, self.style)
yield Segment.line() yield Segment.line()
def __rich_measure__(self, console: Console, max_width: int) -> Measurement: def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return ( return (
Measurement(self.width, self.width) Measurement(self.width, self.width)
if self.width is not None if self.width is not None
else Measurement(4, max_width) else Measurement(4, options.max_width)
) )

View file

@ -77,7 +77,7 @@ class Columns(JupyterMixin):
get_measurement = Measurement.get get_measurement = Measurement.get
renderable_widths = [ renderable_widths = [
get_measurement(console, renderable, max_width).maximum get_measurement(console, options, renderable).maximum
for renderable in renderables for renderable in renderables
] ]
if self.equal: if self.equal:

View file

@ -127,6 +127,8 @@ class ConsoleOptions:
"""Disable wrapping for text.""" """Disable wrapping for text."""
highlight: Optional[bool] = None highlight: Optional[bool] = None
"""Highlight override for render_str.""" """Highlight override for render_str."""
markup: Optional[bool] = None
"""Enable markup when rendering strings."""
height: Optional[int] = None height: Optional[int] = None
"""Height available, or None for no height limit.""" """Height available, or None for no height limit."""
@ -155,6 +157,7 @@ class ConsoleOptions:
overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE, overflow: Union[Optional[OverflowMethod], NoChange] = NO_CHANGE,
no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE, no_wrap: Union[Optional[bool], NoChange] = NO_CHANGE,
highlight: Union[Optional[bool], NoChange] = NO_CHANGE, highlight: Union[Optional[bool], NoChange] = NO_CHANGE,
markup: Union[Optional[bool], NoChange] = NO_CHANGE,
height: Union[Optional[int], NoChange] = NO_CHANGE, height: Union[Optional[int], NoChange] = NO_CHANGE,
) -> "ConsoleOptions": ) -> "ConsoleOptions":
"""Update values, return a copy.""" """Update values, return a copy."""
@ -173,6 +176,8 @@ class ConsoleOptions:
options.no_wrap = no_wrap options.no_wrap = no_wrap
if not isinstance(highlight, NoChange): if not isinstance(highlight, NoChange):
options.highlight = highlight options.highlight = highlight
if not isinstance(markup, NoChange):
options.markup = markup
if not isinstance(height, NoChange): if not isinstance(height, NoChange):
options.height = None if height is None else max(0, height) options.height = None if height is None else max(0, height)
return options return options
@ -405,11 +410,13 @@ class RenderGroup:
self._render = list(self._renderables) self._render = list(self._renderables)
return self._render return self._render
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
if self.fit: if self.fit:
return measure_renderables(console, self.renderables, max_width) return measure_renderables(console, options, self.renderables)
else: else:
return Measurement(max_width, max_width) return Measurement(options.max_width, options.max_width)
def __rich_console__( def __rich_console__(
self, console: "Console", options: "ConsoleOptions" self, console: "Console", options: "ConsoleOptions"
@ -1105,7 +1112,9 @@ class Console:
if hasattr(renderable, "__rich_console__"): if hasattr(renderable, "__rich_console__"):
render_iterable = renderable.__rich_console__(self, _options) # type: ignore render_iterable = renderable.__rich_console__(self, _options) # type: ignore
elif isinstance(renderable, str): elif isinstance(renderable, str):
text_renderable = self.render_str(renderable, highlight=_options.highlight) text_renderable = self.render_str(
renderable, highlight=_options.highlight, markup=_options.markup
)
render_iterable = text_renderable.__rich_console__(self, _options) # type: ignore render_iterable = text_renderable.__rich_console__(self, _options) # type: ignore
else: else:
raise errors.NotRenderableError( raise errors.NotRenderableError(
@ -1467,9 +1476,10 @@ class Console:
render_options = self.options.update( render_options = self.options.update(
justify=justify, justify=justify,
overflow=overflow, overflow=overflow,
width=min(width, self.width) if width else NO_CHANGE, width=min(width, self.width) if width is not None else NO_CHANGE,
height=height, height=height,
no_wrap=no_wrap, no_wrap=no_wrap,
markup=markup,
) )
new_segments: List[Segment] = [] new_segments: List[Segment] = []

View file

@ -28,8 +28,10 @@ class Constrain(JupyterMixin):
child_options = options.update(width=min(self.width, options.max_width)) child_options = options.update(width=min(self.width, options.max_width))
yield from console.render(self.renderable, child_options) yield from console.render(self.renderable, child_options)
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
if self.width is not None: if self.width is not None:
max_width = min(self.width, max_width) max_width = min(self.width, options.max_width)
measurement = Measurement.get(console, self.renderable, max_width) measurement = Measurement.get(console, options, self.renderable)
return measurement return measurement

View file

@ -39,9 +39,11 @@ class Renderables:
"""Console render method to insert line-breaks.""" """Console render method to insert line-breaks."""
yield from self._renderables yield from self._renderables
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
dimensions = [ dimensions = [
Measurement.get(console, renderable, max_width) Measurement.get(console, options, renderable)
for renderable in self._renderables for renderable in self._renderables
] ]
if not dimensions: if not dimensions:

View file

@ -27,8 +27,9 @@ CONTROL_CODES_FORMAT: Dict[int, Callable] = {
ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B", ControlType.CURSOR_DOWN: lambda param: f"\x1b[{param}B",
ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C", ControlType.CURSOR_FORWARD: lambda param: f"\x1b[{param}C",
ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D", ControlType.CURSOR_BACKWARD: lambda param: f"\x1b[{param}D",
ControlType.CURSOR_MOVE_TO_ROW: lambda param: f"\x1b[{param+1}G",
ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K", ControlType.ERASE_IN_LINE: lambda param: f"\x1b[{param}K",
ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y};{x}H", ControlType.CURSOR_MOVE_TO: lambda x, y: f"\x1b[{y+1};{x+1}H",
} }
@ -40,7 +41,7 @@ class Control:
tuple of ControlType and an integer parameter tuple of ControlType and an integer parameter
""" """
__slots__ = ["_segment"] __slots__ = ["segment"]
def __init__(self, *codes: Union[ControlType, ControlCode]) -> None: def __init__(self, *codes: Union[ControlType, ControlCode]) -> None:
control_codes: List[ControlCode] = [ control_codes: List[ControlCode] = [
@ -50,11 +51,7 @@ class Control:
rendered_codes = "".join( rendered_codes = "".join(
_format_map[code](*parameters) for code, *parameters in control_codes _format_map[code](*parameters) for code, *parameters in control_codes
) )
self._segment = Segment(rendered_codes, None, control_codes) self.segment = Segment(rendered_codes, None, control_codes)
@property
def segment(self) -> "Segment":
return self._segment
@classmethod @classmethod
def bell(cls) -> "Control": def bell(cls) -> "Control":
@ -67,7 +64,7 @@ class Control:
return cls(ControlType.HOME) return cls(ControlType.HOME)
@classmethod @classmethod
def move(cls, x: int, y: int) -> "Control": def move(cls, x: int = 0, y: int = 0) -> "Control":
"""Move cursor relative to current position. """Move cursor relative to current position.
Args: Args:
@ -84,18 +81,41 @@ class Control:
if x: if x:
yield ( yield (
control.CURSOR_FORWARD if x > 0 else control.CURSOR_BACKWARD, control.CURSOR_FORWARD if x > 0 else control.CURSOR_BACKWARD,
x, abs(x),
) )
if y: if y:
yield ( yield (
control.CURSOR_DOWN if y > 0 else control.CURSOR_UP, control.CURSOR_DOWN if y > 0 else control.CURSOR_UP,
y, abs(y),
) )
control = cls(*get_codes()) control = cls(*get_codes())
return control return control
@classmethod
def move_to_row(cls, x: int, y: int = 0) -> "Control":
"""Move to the given row, optionally add offset to column.
Returns:
x (int): absolute x (column)
y (int): optional y offset (row)
Returns:
~Control: Control object.
"""
return (
cls(
(ControlType.CURSOR_MOVE_TO_ROW, x + 1),
(
ControlType.CURSOR_DOWN if y > 0 else ControlType.CURSOR_UP,
abs(y),
),
)
if y
else cls((ControlType.CURSOR_MOVE_TO_ROW, x))
)
@classmethod @classmethod
def move_to(cls, x: int, y: int) -> "Control": def move_to(cls, x: int, y: int) -> "Control":
"""Move cursor to absolute position. """Move cursor to absolute position.
@ -107,7 +127,7 @@ class Control:
Returns: Returns:
~Control: Control object. ~Control: Control object.
""" """
return cls((ControlType.CURSOR_MOVE_TO, x + 1, y + 1)) return cls((ControlType.CURSOR_MOVE_TO, x, y))
@classmethod @classmethod
def clear(cls) -> "Control": def clear(cls) -> "Control":
@ -128,12 +148,12 @@ class Control:
return cls(ControlType.DISABLE_ALT_SCREEN) return cls(ControlType.DISABLE_ALT_SCREEN)
def __str__(self) -> str: def __str__(self) -> str:
return self._segment.text return self.segment.text
def __rich_console__( def __rich_console__(
self, console: "Console", options: "ConsoleOptions" self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult": ) -> "RenderResult":
yield self._segment yield self.segment
def strip_control_codes(text: str, _translate_table=_CONTROL_TRANSLATE) -> str: def strip_control_codes(text: str, _translate_table=_CONTROL_TRANSLATE) -> str:

View file

@ -4,7 +4,6 @@ from operator import itemgetter
from threading import RLock from threading import RLock
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any,
Dict, Dict,
Iterable, Iterable,
List, List,
@ -17,7 +16,6 @@ from typing import (
from typing_extensions import Literal from typing_extensions import Literal
from ._loop import loop_last
from ._ratio import ratio_resolve from ._ratio import ratio_resolve
from .align import Align from .align import Align
from .console import Console, ConsoleOptions, RenderableType, RenderResult from .console import Console, ConsoleOptions, RenderableType, RenderResult
@ -327,7 +325,7 @@ class Layout:
with self._lock: with self._lock:
self._renderable = renderable self._renderable = renderable
def refresh(self, console: "Console", layout_name: str) -> None: def refresh_screen(self, console: "Console", layout_name: str) -> None:
"""Refresh a sub-layout. """Refresh a sub-layout.
Args: Args:
@ -376,7 +374,7 @@ class Layout:
RenderMap: A dict that maps Layout on to a tuple of Region, lines RenderMap: A dict that maps Layout on to a tuple of Region, lines
""" """
render_width = options.max_width render_width = options.max_width
render_height = options.height or 1 render_height = options.height or console.height
region_map = self._make_region_map(render_width, render_height) region_map = self._make_region_map(render_width, render_height)
layout_regions = [ layout_regions = [
(layout, region) (layout, region)
@ -402,7 +400,7 @@ class Layout:
height = options.height or console.height height = options.height or console.height
render_map = self.render(console, options.update_dimensions(width, height)) render_map = self.render(console, options.update_dimensions(width, height))
self._render_map = render_map self._render_map = render_map
layout_lines: List[List[Segment]] = [[] for _ in range(options.height or 1)] layout_lines: List[List[Segment]] = [[] for _ in range(height)]
_islice = islice _islice = islice
for (region, lines) in render_map.values(): for (region, lines) in render_map.values():
_x, y, _layout_width, layout_height = region _x, y, _layout_width, layout_height = region
@ -442,19 +440,4 @@ if __name__ == "__main__": # type: ignore
layout["content"].update("foo") layout["content"].update("foo")
from rich.live import Live console.print(layout)
from time import sleep
from rich import print
from rich.pretty import Pretty
# l = Layout()
# # l.split(Layout(), Layout())
# print(l.tree)
with Live(layout, console=console, screen=True, refresh_per_second=1) as live:
for n in range(100):
layout["top"].update("[on red]" + str(n))
sleep(0.1)
layout.refresh(console, "top")

View file

@ -5,7 +5,7 @@ from . import errors
from .protocol import is_renderable from .protocol import is_renderable
if TYPE_CHECKING: if TYPE_CHECKING:
from .console import Console, RenderableType from .console import Console, ConsoleOptions, RenderableType
class Measurement(NamedTuple): class Measurement(NamedTuple):
@ -75,15 +75,14 @@ class Measurement(NamedTuple):
@classmethod @classmethod
def get( def get(
cls, console: "Console", renderable: "RenderableType", max_width: int = None cls, console: "Console", options: "ConsoleOptions", renderable: "RenderableType"
) -> "Measurement": ) -> "Measurement":
"""Get a measurement for a renderable. """Get a measurement for a renderable.
Args: Args:
console (~rich.console.Console): Console instance. console (~rich.console.Console): Console instance.
options (~rich.console.ConsoleOptions): Console options.
renderable (RenderableType): An object that may be rendered with Rich. renderable (RenderableType): An object that may be rendered with Rich.
max_width (int, optional): The maximum width available, or None to use console.width.
Defaults to None.
Raises: Raises:
errors.NotRenderableError: If the object is not renderable. errors.NotRenderableError: If the object is not renderable.
@ -91,20 +90,18 @@ class Measurement(NamedTuple):
Returns: Returns:
Measurement: Measurement object containing range of character widths required to render the object. Measurement: Measurement object containing range of character widths required to render the object.
""" """
_max_width = console.width if max_width is None else max_width _max_width = options.max_width
if _max_width < 1: if _max_width < 1:
return Measurement(0, 0) return Measurement(0, 0)
if isinstance(renderable, str): if isinstance(renderable, str):
renderable = console.render_str(renderable) renderable = console.render_str(renderable, markup=options.markup)
if hasattr(renderable, "__rich__"): if hasattr(renderable, "__rich__"):
renderable = renderable.__rich__() # type: ignore renderable = renderable.__rich__() # type: ignore
if is_renderable(renderable): if is_renderable(renderable):
get_console_width = getattr(renderable, "__rich_measure__", None) get_console_width = getattr(renderable, "__rich_measure__", None)
if get_console_width is not None: if get_console_width is not None:
render_width = ( render_width = (
get_console_width(console, _max_width) get_console_width(console, options)
.normalize() .normalize()
.with_maximum(_max_width) .with_maximum(_max_width)
) )
@ -121,7 +118,9 @@ class Measurement(NamedTuple):
def measure_renderables( def measure_renderables(
console: "Console", renderables: Iterable["RenderableType"], max_width: int console: "Console",
options: "ConsoleOptions",
renderables: Iterable["RenderableType"],
) -> "Measurement": ) -> "Measurement":
"""Get a measurement that would fit a number of renderables. """Get a measurement that would fit a number of renderables.
@ -138,7 +137,7 @@ def measure_renderables(
return Measurement(0, 0) return Measurement(0, 0)
get_measurement = Measurement.get get_measurement = Measurement.get
measurements = [ measurements = [
get_measurement(console, renderable, max_width) for renderable in renderables get_measurement(console, options, renderable) for renderable in renderables
] ]
measured_width = Measurement( measured_width = Measurement(
max(measurements, key=itemgetter(0)).minimum, max(measurements, key=itemgetter(0)).minimum,

View file

@ -84,7 +84,7 @@ class Padding(JupyterMixin):
width = options.max_width width = options.max_width
else: else:
width = min( width = min(
Measurement.get(console, self.renderable, options.max_width).maximum Measurement.get(console, options, self.renderable).maximum
+ self.left + self.left
+ self.right, + self.right,
options.max_width, options.max_width,
@ -120,13 +120,14 @@ class Padding(JupyterMixin):
blank_line = blank_line or [_Segment(f'{" " * width}\n', style)] blank_line = blank_line or [_Segment(f'{" " * width}\n', style)]
yield from blank_line * self.bottom yield from blank_line * self.bottom
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
max_width = options.max_width
extra_width = self.left + self.right extra_width = self.left + self.right
if max_width - extra_width < 1: if max_width - extra_width < 1:
return Measurement(max_width, max_width) return Measurement(max_width, max_width)
measure_min, measure_max = Measurement.get( measure_min, measure_max = Measurement.get(console, options, self.renderable)
console, self.renderable, max(0, max_width - extra_width)
)
measurement = Measurement(measure_min + extra_width, measure_max + extra_width) measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
measurement = measurement.with_maximum(max_width) measurement = measurement.with_maximum(max_width)
return measurement return measurement

View file

@ -133,7 +133,9 @@ class Panel(JupyterMixin):
child_width = ( child_width = (
width - 2 width - 2
if self.expand if self.expand
else Measurement.get(console, renderable, width - 2).maximum else Measurement.get(
console, options.update_width(width - 2), renderable
).maximum
) )
child_height = self.height or options.height or None child_height = self.height or options.height or None
if child_height: if child_height:
@ -169,7 +171,9 @@ class Panel(JupyterMixin):
yield Segment(box.get_bottom([width - 2]), border_style) yield Segment(box.get_bottom([width - 2]), border_style)
yield new_line yield new_line
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
_title = self._title _title = self._title
_, right, _, left = Padding.unpack(self.padding) _, right, _, left = Padding.unpack(self.padding)
padding = left + right padding = left + right
@ -178,7 +182,9 @@ class Panel(JupyterMixin):
if self.width is None: if self.width is None:
width = ( width = (
measure_renderables( measure_renderables(
console, renderables, max_width - padding - 2 console,
options.update_width(options.max_width - padding - 2),
renderables,
).maximum ).maximum
+ padding + padding
+ 2 + 2

View file

@ -212,10 +212,12 @@ class Pretty:
yield "" yield ""
yield pretty_text yield pretty_text
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
pretty_str = pretty_repr( pretty_str = pretty_repr(
self._object, self._object,
max_width=max_width, max_width=options.max_width,
indent_size=self.indent_size, indent_size=self.indent_size,
max_length=self.max_length, max_length=self.max_length,
max_string=self.max_string, max_string=self.max_string,
@ -449,24 +451,26 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No
py_version = (sys.version_info.major, sys.version_info.minor) py_version = (sys.version_info.major, sys.version_info.minor)
children: List[Node] children: List[Node]
def iter_tokens(tokens) -> Iterable[Union[Any, Tuple[str, Any]]]: def iter_rich_args(rich_args) -> Iterable[Union[Any, Tuple[str, Any]]]:
for token in tokens: for arg in rich_args:
if isinstance(token, tuple): if isinstance(arg, tuple):
if len(token) == 3: if len(arg) == 3:
key, child, default = token key, child, default = arg
if default == child: if default == child:
continue continue
yield key, child yield key, child
elif len(token) == 2: elif len(arg) == 2:
key, child = token key, child = arg
yield key, child yield key, child
elif len(arg) == 1:
yield arg[0]
else: else:
yield token yield arg
if hasattr(obj, "__rich_repr__"): if hasattr(obj, "__rich_repr__"):
tokens = list(iter_tokens(obj.__rich_repr__())) args = list(iter_rich_args(obj.__rich_repr__()))
if tokens: if args:
children = [] children = []
append = children.append append = children.append
node = Node( node = Node(
@ -475,9 +479,9 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No
children=children, children=children,
last=root, last=root,
) )
for last, token in loop_last(tokens): for last, arg in loop_last(args):
if isinstance(token, tuple): if isinstance(arg, tuple):
key, child = token key, child = arg
child_node = _traverse(child) child_node = _traverse(child)
child_node.last = last child_node.last = last
child_node.key_repr = key child_node.key_repr = key
@ -485,7 +489,7 @@ def traverse(_object: Any, max_length: int = None, max_string: int = None) -> No
child_node.key_separator = "=" child_node.key_separator = "="
append(child_node) append(child_node)
else: else:
child_node = _traverse(token) child_node = _traverse(arg)
child_node.last = last child_node.last = last
append(child_node) append(child_node)
else: else:

View file

@ -190,11 +190,13 @@ class ProgressBar(JupyterMixin):
if remaining_bars: if remaining_bars:
yield _Segment(bar * remaining_bars, style) yield _Segment(bar * remaining_bars, style)
def __rich_measure__(self, console: Console, max_width: int) -> Measurement: def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return ( return (
Measurement(self.width, self.width) Measurement(self.width, self.width)
if self.width is not None if self.width is not None
else Measurement(4, max_width) else Measurement(4, options.max_width)
) )

View file

@ -13,14 +13,17 @@ def rich_repr(cls: Type[T]) -> Type[T]:
def auto_repr(self) -> str: def auto_repr(self) -> str:
repr_str: List[str] = [] repr_str: List[str] = []
append = repr_str.append append = repr_str.append
for token in self.__rich_repr__(): for arg in self.__rich_repr__():
if isinstance(token, tuple): if isinstance(arg, tuple):
key, value, *default = token if len(arg) == 1:
if len(default) and default[0] == value: append(repr(arg[0]))
continue else:
append(f"{key}={value!r}") key, value, *default = arg
if len(default) and default[0] == value:
continue
append(f"{key}={value!r}")
else: else:
append(repr(token)) append(repr(arg))
return f"{self.__class__.__name__}({', '.join(repr_str)})" return f"{self.__class__.__name__}({', '.join(repr_str)})"
auto_repr.__doc__ = "Return repr(self)" auto_repr.__doc__ = "Return repr(self)"

View file

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .measure import Measurement
from .segment import Segment from .segment import Segment
from .style import StyleType from .style import StyleType
from ._loop import loop_last from ._loop import loop_last

View file

@ -25,8 +25,9 @@ class ControlType(IntEnum):
CURSOR_DOWN = 10 CURSOR_DOWN = 10
CURSOR_FORWARD = 11 CURSOR_FORWARD = 11
CURSOR_BACKWARD = 12 CURSOR_BACKWARD = 12
CURSOR_MOVE_TO = 13 CURSOR_MOVE_TO_ROW = 13
ERASE_IN_LINE = 14 CURSOR_MOVE_TO = 14
ERASE_IN_LINE = 15
ControlCode = Union[ ControlCode = Union[

View file

@ -46,9 +46,11 @@ class Spinner:
text = self.render(time - self.start_time) text = self.render(time - self.start_time)
yield text yield text
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
text = self.render(0) text = self.render(0)
return Measurement.get(console, text, max_width) return Measurement.get(console, options, text)
def render(self, time: float) -> Text: def render(self, time: float) -> Text:
"""Render the spinner for a given time. """Render the spinner for a given time.

View file

@ -28,8 +28,10 @@ class Styled:
segments = Segment.apply_style(rendered_segments, style) segments = Segment.apply_style(rendered_segments, style)
return segments return segments
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
return Measurement.get(console, self.renderable, max_width) self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
return Measurement.get(console, options, self.renderable)
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover

View file

@ -468,11 +468,13 @@ class Syntax(JupyterMixin):
highlight_number_style = background_style + Style(dim=False) highlight_number_style = background_style + Style(dim=False)
return background_style, number_style, highlight_number_style return background_style, number_style, highlight_number_style
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
if self.code_width is not None: if self.code_width is not None:
width = self.code_width + self._numbers_column_width width = self.code_width + self._numbers_column_width
return Measurement(self._numbers_column_width, width) return Measurement(self._numbers_column_width, width)
return Measurement(self._numbers_column_width, max_width) return Measurement(self._numbers_column_width, options.max_width)
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions

View file

@ -281,18 +281,26 @@ class Table(JupyterMixin):
style += console.get_style(row_style) style += console.get_style(row_style)
return style return style
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
max_width = options.max_width
if self.width is not None: if self.width is not None:
max_width = self.width max_width = self.width
if max_width < 0: if max_width < 0:
return Measurement(0, 0) return Measurement(0, 0)
extra_width = self._extra_width extra_width = self._extra_width
max_width = sum(self._calculate_column_widths(console, max_width - extra_width)) max_width = sum(
self._calculate_column_widths(
console, options.update_width(max_width - extra_width)
)
)
_measure_column = self._measure_column _measure_column = self._measure_column
measurements = [ measurements = [
_measure_column(console, column, max_width) for column in self.columns _measure_column(console, options.update_width(max_width), column)
for column in self.columns
] ]
minimum_width = ( minimum_width = (
sum(measurement.minimum for measurement in measurements) + extra_width sum(measurement.minimum for measurement in measurements) + extra_width
@ -428,7 +436,9 @@ class Table(JupyterMixin):
max_width = self.width max_width = self.width
extra_width = self._extra_width extra_width = self._extra_width
widths = self._calculate_column_widths(console, max_width - extra_width) widths = self._calculate_column_widths(
console, options.update_width(max_width - extra_width)
)
table_width = sum(widths) + extra_width table_width = sum(widths) + extra_width
render_options = options.update( render_options = options.update(
@ -461,11 +471,14 @@ class Table(JupyterMixin):
justify=self.caption_justify, justify=self.caption_justify,
) )
def _calculate_column_widths(self, console: "Console", max_width: int) -> List[int]: def _calculate_column_widths(
self, console: "Console", options: "ConsoleOptions"
) -> List[int]:
"""Calculate the widths of each column, including padding, not including borders.""" """Calculate the widths of each column, including padding, not including borders."""
max_width = options.max_width
columns = self.columns columns = self.columns
width_ranges = [ width_ranges = [
self._measure_column(console, column, max_width) for column in columns self._measure_column(console, options, column) for column in columns
] ]
widths = [_range.maximum or 1 for _range in width_ranges] widths = [_range.maximum or 1 for _range in width_ranges]
get_padding_width = self._get_padding_width get_padding_width = self._get_padding_width
@ -504,7 +517,7 @@ class Table(JupyterMixin):
table_width = sum(widths) table_width = sum(widths)
width_ranges = [ width_ranges = [
self._measure_column(console, column, width) self._measure_column(console, options.update_width(width), column)
for width, column in zip(widths, columns) for width, column in zip(widths, columns)
] ]
widths = [_range.maximum or 0 for _range in width_ranges] widths = [_range.maximum or 0 for _range in width_ranges]
@ -609,7 +622,7 @@ class Table(JupyterMixin):
column.header_style column.header_style
) )
_append((header_style, column.header)) _append((header_style, column.header))
cell_style = get_style(self.style or "") + get_style(column.style or "") cell_style = get_style(column.style or "")
for cell in column.cells: for cell in column.cells:
_append((cell_style, cell)) _append((cell_style, cell))
if self.show_footer: if self.show_footer:
@ -635,10 +648,14 @@ class Table(JupyterMixin):
return pad_left + pad_right return pad_left + pad_right
def _measure_column( def _measure_column(
self, console: "Console", column: Column, max_width: int self,
console: "Console",
options: "ConsoleOptions",
column: Column,
) -> Measurement: ) -> Measurement:
"""Get the minimum and maximum width of the column.""" """Get the minimum and maximum width of the column."""
max_width = options.max_width
if max_width < 1: if max_width < 1:
return Measurement(0, 0) return Measurement(0, 0)
@ -656,7 +673,7 @@ class Table(JupyterMixin):
append_max = max_widths.append append_max = max_widths.append
get_render_width = Measurement.get get_render_width = Measurement.get
for cell in self._get_cells(console, column._index, column): for cell in self._get_cells(console, column._index, column):
_min, _max = get_render_width(console, cell.renderable, max_width) _min, _max = get_render_width(console, options, cell.renderable)
append_min(_min) append_min(_min)
append_max(_max) append_max(_max)

View file

@ -525,12 +525,16 @@ class Text(JupyterMixin):
all_lines = Text("\n").join(lines) all_lines = Text("\n").join(lines)
yield from all_lines.render(console, end=self.end) yield from all_lines.render(console, end=self.end)
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> Measurement:
text = self.plain text = self.plain
if not text.strip(): lines = text.splitlines()
return Measurement(cell_len(text), cell_len(text)) max_text_width = max(cell_len(line) for line in lines) if lines else 0
max_text_width = max(cell_len(line) for line in text.splitlines()) words = text.split()
min_text_width = max(cell_len(word) for word in text.split()) min_text_width = (
max(cell_len(word) for word in words) if words else max_text_width
)
return Measurement(min_text_width, max_text_width) return Measurement(min_text_width, max_text_width)
def render(self, console: "Console", end: str = "") -> Iterable["Segment"]: def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:

View file

@ -158,7 +158,9 @@ class Tree(JupyterMixin):
guide_style_stack.push(get_style(node.guide_style)) guide_style_stack.push(get_style(node.guide_style))
push(iter(loop_last(node.children))) push(iter(loop_last(node.children)))
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(
self, console: "Console", options: "ConsoleOptions"
) -> "Measurement":
stack: List[Iterator[Tree]] = [iter([self])] stack: List[Iterator[Tree]] = [iter([self])]
pop = stack.pop pop = stack.pop
push = stack.append push = stack.append
@ -174,7 +176,7 @@ class Tree(JupyterMixin):
level -= 1 level -= 1
continue continue
push(iter_tree) push(iter_tree)
min_measure, max_measure = measure(console, tree.label, max_width) min_measure, max_measure = measure(console, options, tree.label)
indent = level * 4 indent = level * 4
minimum = max(min_measure + indent, minimum) minimum = max(min_measure + indent, minimum)
maximum = max(max_measure + indent, maximum) maximum = max(max_measure + indent, maximum)

View file

@ -107,7 +107,7 @@ def test_align_right_style():
def test_measure(): def test_measure():
console = Console(file=io.StringIO(), width=20) console = Console(file=io.StringIO(), width=20)
_min, _max = Measurement.get(console, Align("foo bar", "left"), 20) _min, _max = Measurement.get(console, console.options, Align("foo bar", "left"))
assert _min == 3 assert _min == 3
assert _max == 7 assert _max == 7
@ -147,4 +147,6 @@ def test_vertical_center():
print(repr(result)) print(repr(result))
expected = " \n \nfoo\n \n \n \n" expected = " \n \nfoo\n \n \n \n"
assert result == expected assert result == expected
assert Measurement.get(console, vertical_center) == Measurement(3, 3) assert Measurement.get(console, console.options, vertical_center) == Measurement(
3, 3
)

View file

@ -1,3 +1,4 @@
from rich.console import Console
from rich.progress_bar import ProgressBar from rich.progress_bar import ProgressBar
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
@ -39,8 +40,9 @@ def test_render():
def test_measure(): def test_measure():
console = Console(width=120)
bar = ProgressBar() bar = ProgressBar()
measurement = bar.__rich_measure__(None, 120) measurement = bar.__rich_measure__(console, console.options)
assert measurement.minimum == 4 assert measurement.minimum == 4
assert measurement.maximum == 120 assert measurement.maximum == 120

View file

@ -1,4 +1,5 @@
from rich.bar import Bar from rich.bar import Bar
from rich.console import Console
from .render import render from .render import render
@ -29,8 +30,9 @@ def test_render():
def test_measure(): def test_measure():
console = Console(width=120)
bar = Bar(size=100, begin=11, end=62) bar = Bar(size=100, begin=11, end=62)
measurement = bar.__rich_measure__(None, 120) measurement = bar.__rich_measure__(console, console.options)
assert measurement.minimum == 4 assert measurement.minimum == 4
assert measurement.maximum == 120 assert measurement.maximum == 120

View file

@ -475,7 +475,7 @@ def test_render_group() -> None:
renderables = [renderable() for _ in range(4)] renderables = [renderable() for _ in range(4)]
console = Console(width=42) console = Console(width=42)
min_width, _ = measure_renderables(console, renderables, 42) min_width, _ = measure_renderables(console, console.options, renderables)
assert min_width == 42 assert min_width == 42
@ -491,7 +491,7 @@ def test_render_group_fit() -> None:
console = Console(width=42) console = Console(width=42)
min_width, _ = measure_renderables(console, renderables, 42) min_width, _ = measure_renderables(console, console.options, renderables)
assert min_width == 5 assert min_width == 5
@ -633,3 +633,17 @@ def test_update_screen_lines():
console = Console(force_terminal=True, width=20, height=5) console = Console(force_terminal=True, width=20, height=5)
with pytest.raises(errors.NoAltScreen): with pytest.raises(errors.NoAltScreen):
console.update_screen_lines([]) console.update_screen_lines([])
def test_update_options_markup():
console = Console()
options = console.options
assert options.update(markup=False).markup == False
assert options.update(markup=True).markup == True
def test_print_width_zero():
console = Console()
with console.capture() as capture:
console.print("Hello", width=0)
assert capture.get() == ""

View file

@ -6,6 +6,8 @@ from rich.text import Text
def test_width_of_none(): def test_width_of_none():
console = Console() console = Console()
constrain = Constrain(Text("foo"), width=None) constrain = Constrain(Text("foo"), width=None)
min_width, max_width = constrain.__rich_measure__(console, 80) min_width, max_width = constrain.__rich_measure__(
console, console.options.update_width(80)
)
assert min_width == 3 assert min_width == 3
assert max_width == 3 assert max_width == 3

View file

@ -9,7 +9,7 @@ def test_renderables_measure():
text = Text("foo") text = Text("foo")
renderables = Renderables([text]) renderables = Renderables([text])
result = renderables.__rich_measure__(console, console.width) result = renderables.__rich_measure__(console, console.options)
_min, _max = result _min, _max = result
assert _min == 3 assert _min == 3
assert _max == 3 assert _max == 3
@ -21,7 +21,7 @@ def test_renderables_empty():
console = Console() console = Console()
renderables = Renderables() renderables = Renderables()
result = renderables.__rich_measure__(console, console.width) result = renderables.__rich_measure__(console, console.options)
_min, _max = result _min, _max = result
assert _min == 1 assert _min == 1
assert _max == 1 assert _max == 1

View file

@ -17,7 +17,7 @@ def test_control_move_to():
control = Control.move_to(5, 10) control = Control.move_to(5, 10)
print(control.segment) print(control.segment)
assert control.segment == Segment( assert control.segment == Segment(
"\x1b[11;6H", None, [(ControlType.CURSOR_MOVE_TO, 6, 11)] "\x1b[11;6H", None, [(ControlType.CURSOR_MOVE_TO, 5, 10)]
) )
@ -30,3 +30,12 @@ def test_control_move():
None, None,
[(ControlType.CURSOR_FORWARD, 3), (ControlType.CURSOR_DOWN, 4)], [(ControlType.CURSOR_FORWARD, 3), (ControlType.CURSOR_DOWN, 4)],
) )
def test_move_to_row():
print(repr(Control.move_to_row(10, 20).segment))
assert Control.move_to_row(10, 20).segment == Segment(
"\x1b[12G\x1b[20B",
None,
[(ControlType.CURSOR_MOVE_TO_ROW, 11), (ControlType.CURSOR_DOWN, 20)],
)

View file

@ -76,3 +76,17 @@ def test_tree():
expected = "⬍ Layout(name='root') \n├── ⬍ Layout(size=2) \n└── ⬌ Layout(name='bar') \n ├── ⬍ Layout() \n └── ⬍ Layout() \n" expected = "⬍ Layout(name='root') \n├── ⬍ Layout(size=2) \n└── ⬌ Layout(name='bar') \n ├── ⬍ Layout() \n └── ⬍ Layout() \n"
assert result == expected assert result == expected
def test_refresh_screen():
layout = Layout()
layout.split_row(Layout(name="foo"), Layout(name="bar"))
console = Console(force_terminal=True, width=20, height=5)
console.print(layout)
with console.screen():
with console.capture() as capture:
layout.refresh_screen(console, "foo")
result = capture.get()
print(repr(result))
expected = "\x1b[1;1H\x1b[34m╭─\x1b[0m\x1b[34m \x1b[0m\x1b[32m'foo'\x1b[0m\x1b[34m─╮\x1b[0m\x1b[2;1H\x1b[34m│\x1b[0m Layout \x1b[34m│\x1b[0m\x1b[3;1H\x1b[34m│\x1b[0m \x1b[1m(\x1b[0m \x1b[34m│\x1b[0m\x1b[4;1H\x1b[34m│\x1b[0m \x1b[33mna\x1b[0m \x1b[34m│\x1b[0m\x1b[5;1H\x1b[34m╰────────╯\x1b[0m"
assert result == expected

View file

@ -16,21 +16,15 @@ def test_no_renderable():
text = Text() text = Text()
with pytest.raises(NotRenderableError): with pytest.raises(NotRenderableError):
Measurement.get(console, None, console.width) Measurement.get(console, console.options, None)
def test_null_get():
# Test negative console.width passed into get method
assert Measurement.get(Console(width=-1), None) == Measurement(0, 0)
# Test negative max_width passed into get method
assert Measurement.get(Console(), None, -1) == Measurement(0, 0)
def test_measure_renderables(): def test_measure_renderables():
# Test measure_renderables returning a null Measurement object console = Console()
assert measure_renderables(Console(), None, None) == Measurement(0, 0) assert measure_renderables(console, console.options, "") == Measurement(0, 0)
# Test measure_renderables returning a valid Measurement object assert measure_renderables(
assert measure_renderables(Console(width=1), ["test"], 1) == Measurement(1, 1) console, console.options.update_width(0), "hello"
) == Measurement(0, 0)
def test_clamp(): def test_clamp():

View file

@ -38,7 +38,7 @@ def test_render_panel(panel, expected):
def test_console_width(): def test_console_width():
console = Console(file=io.StringIO(), width=50, legacy_windows=False) console = Console(file=io.StringIO(), width=50, legacy_windows=False)
panel = Panel("Hello, World", expand=False) panel = Panel("Hello, World", expand=False)
min_width, max_width = panel.__rich_measure__(console, 50) min_width, max_width = panel.__rich_measure__(console, console.options)
assert min_width == 16 assert min_width == 16
assert max_width == 16 assert max_width == 16
@ -46,7 +46,7 @@ def test_console_width():
def test_fixed_width(): def test_fixed_width():
console = Console(file=io.StringIO(), width=50, legacy_windows=False) console = Console(file=io.StringIO(), width=50, legacy_windows=False)
panel = Panel("Hello World", width=20) panel = Panel("Hello World", width=20)
min_width, max_width = panel.__rich_measure__(console, 100) min_width, max_width = panel.__rich_measure__(console, console.options)
assert min_width == 20 assert min_width == 20
assert max_width == 20 assert max_width == 20

View file

@ -4,18 +4,21 @@ from rich.repr import rich_repr
@rich_repr @rich_repr
class Foo: class Foo:
def __init__(self, foo: str, bar: int = None): def __init__(self, foo: str, bar: int = None, egg: int = 1):
self.foo = foo self.foo = foo
self.bar = bar self.bar = bar
self.egg = egg
def __rich_repr__(self): def __rich_repr__(self):
yield self.foo yield self.foo
yield self.foo,
yield "bar", self.bar, None yield "bar", self.bar, None
yield "egg", self.egg
def test_rich_repr() -> None: def test_rich_repr() -> None:
assert (repr(Foo("hello"))) == "Foo('hello')" assert (repr(Foo("hello"))) == "Foo('hello', 'hello', egg=1)"
assert (repr(Foo("hello", bar=3))) == "Foo('hello', bar=3)" assert (repr(Foo("hello", bar=3))) == "Foo('hello', 'hello', bar=3, egg=1)"
def test_rich_pretty() -> None: def test_rich_pretty() -> None:
@ -23,5 +26,5 @@ def test_rich_pretty() -> None:
with console.capture() as capture: with console.capture() as capture:
console.print(Foo("hello", bar=3)) console.print(Foo("hello", bar=3))
result = capture.get() result = capture.get()
expected = "Foo('hello', bar=3)\n" expected = "Foo('hello', 'hello', bar=3, egg=1)\n"
assert result == expected assert result == expected

View file

@ -37,6 +37,6 @@ def test_spinner_render():
def test_rich_measure(): def test_rich_measure():
console = Console(width=80, color_system=None, force_terminal=True) console = Console(width=80, color_system=None, force_terminal=True)
spinner = Spinner("dots", "Foo") spinner = Spinner("dots", "Foo")
min_width, max_width = Measurement.get(console, spinner, 80) min_width, max_width = Measurement.get(console, console.options, spinner)
assert min_width == 3 assert min_width == 3
assert max_width == 5 assert max_width == 5

View file

@ -8,7 +8,7 @@ from rich.styled import Styled
def test_styled(): def test_styled():
styled_foo = Styled("foo", "on red") styled_foo = Styled("foo", "on red")
console = Console(file=io.StringIO(), force_terminal=True, _environ={}) console = Console(file=io.StringIO(), force_terminal=True, _environ={})
assert Measurement.get(console, styled_foo, 80) == Measurement(3, 3) assert Measurement.get(console, console.options, styled_foo) == Measurement(3, 3)
console.print(styled_foo) console.print(styled_foo)
result = console.file.getvalue() result = console.file.getvalue()
expected = "\x1b[41mfoo\x1b[0m\n" expected = "\x1b[41mfoo\x1b[0m\n"

View file

@ -28,9 +28,9 @@ def render_tables():
table.add_row("Averlongwordgoeshere", "banana pancakes", None) table.add_row("Averlongwordgoeshere", "banana pancakes", None)
assert Measurement.get(console, table, 80) == Measurement(41, 48) assert Measurement.get(console, console.options, table) == Measurement(41, 48)
table.expand = True table.expand = True
assert Measurement.get(console, table, 80) == Measurement(41, 48) assert Measurement.get(console, console.options, table) == Measurement(41, 48)
for width in range(10, 60, 5): for width in range(10, 60, 5):
console.print(table, width=width) console.print(table, width=width)
@ -64,9 +64,9 @@ def render_tables():
console.print(table) console.print(table)
table.width = 20 table.width = 20
assert Measurement.get(console, table, 80) == Measurement(20, 20) assert Measurement.get(console, console.options, table) == Measurement(20, 20)
table.expand = False table.expand = False
assert Measurement.get(console, table, 80) == Measurement(20, 20) assert Measurement.get(console, console.options, table) == Measurement(20, 20)
table.expand = True table.expand = True
console.print(table) console.print(table)
@ -113,28 +113,24 @@ def test_init_append_column():
def test_rich_measure(): def test_rich_measure():
# Check __rich_measure__() for a negative width passed as an argument
assert Table("test_header", width=None).__rich_measure__( console = Console()
Console(), -1 assert Table("test_header", width=-1).__rich_measure__(
console, console.options
) == Measurement(0, 0) ) == Measurement(0, 0)
# Check __rich_measure__() for a negative Table.width attribute
assert Table("test_header", width=-1).__rich_measure__(Console(), 1) == Measurement(
0, 0
)
# Check __rich_measure__() for a positive width passed as an argument # Check __rich_measure__() for a positive width passed as an argument
assert Table("test_header", width=None).__rich_measure__( assert Table("test_header", width=None).__rich_measure__(
Console(), 10 console, console.options.update_width(10)
) == Measurement(10, 10)
# Check __rich_measure__() for a positive Table.width attribute
assert Table("test_header", width=10).__rich_measure__(
Console(), -1
) == Measurement(10, 10) ) == Measurement(10, 10)
def test_min_width(): def test_min_width():
table = Table("foo", min_width=30) table = Table("foo", min_width=30)
table.add_row("bar") table.add_row("bar")
assert table.__rich_measure__(Console(), 100) == Measurement(30, 30) console = Console()
assert table.__rich_measure__(
console, console.options.update_width(100)
) == Measurement(30, 30)
console = Console(color_system=None) console = Console(color_system=None)
console.begin_capture() console.begin_capture()
console.print(table) console.print(table)

View file

@ -238,6 +238,7 @@ def test_console_width():
test = Text("Hello World!\nfoobarbaz") test = Text("Hello World!\nfoobarbaz")
assert test.__rich_measure__(console, 80) == Measurement(9, 12) assert test.__rich_measure__(console, 80) == Measurement(9, 12)
assert Text(" " * 4).__rich_measure__(console, 80) == Measurement(4, 4) assert Text(" " * 4).__rich_measure__(console, 80) == Measurement(4, 4)
assert Text(" \n \n ").__rich_measure__(console, 80) == Measurement(3, 3)
def test_join(): def test_join():

View file

@ -99,5 +99,5 @@ def test_tree_measure():
tree.add("bar") tree.add("bar")
tree.add("musroom risotto") tree.add("musroom risotto")
console = Console() console = Console()
measurement = Measurement.get(console, tree) measurement = Measurement.get(console, console.options, tree)
assert measurement == Measurement(11, 19) assert measurement == Measurement(11, 19)