This commit is contained in:
Will McGugan 2020-06-06 12:08:07 +01:00
parent f7318cea1b
commit 47bb80ff67
17 changed files with 199 additions and 187 deletions

View File

@ -8,12 +8,14 @@
# Rich
Rich is a Python library for rendering _rich_ text and beautiful formatting to the terminal.
Rich is a Python library for _rich_ text and beautiful formatting in the terminal.
The [Rich API](https://rich.readthedocs.io/en/latest/) makes it easy to add colorful text (up to 16.7 million colors) with styles (bold, italic, underline etc.) to your script or application. Rich can also render pretty tables, progress bars, markdown, syntax highlighted source code, and tracebacks -- out of the box.
![Features](https://github.com/willmcgugan/rich/raw/master/imgs/features.png)
For a video introduction to Rich see [calmcode.io](https://calmcode.io/rich/introduction.html).
## Compatibility
Rich works with Linux, OSX, and Windows. True color / emoji works with new Windows Terminal, classic terminal is limited to 8 colors.

11
examples/overflow.py Normal file
View File

@ -0,0 +1,11 @@
from typing import List
from rich.console import Console, OverflowMethod
from rich.text import Text
console = Console()
supercali = "supercalifragilisticexpialidocious"
overflow_methods: List[OverflowMethod] = ["fold", "crop", "ellipsis"]
for overflow in overflow_methods:
console.rule(overflow)
console.print(supercali, overflow=overflow, width=10)

View File

@ -48,7 +48,7 @@ class LogRender:
output.add_column(style="log.time")
if self.show_level:
output.add_column(style="log.level", width=8)
output.add_column(ratio=1, style="log.message", justify=None)
output.add_column(ratio=1, style="log.message")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []

View File

@ -3,34 +3,6 @@ from math import ceil
from typing import List
def ratio_distribute(total: int, ratios: List[int]) -> List[int]:
"""Divide an integer total in to parts based on ratios.
Args:
total (int): The total to divide.
ratios (List[int]): A list of integer ratios.
minimums (List[int]): List of minimum values for each slot.
Returns:
List[int]: A list of integers garanteed to sum to total.
"""
total_ratio = sum(ratios)
assert total_ratio > 0, "Sum of ratios must be > 0"
total_remaining = total
distributed_total: List[int] = []
append = distributed_total.append
for ratio in ratios:
if total_ratio > 0:
distributed = int(ceil(ratio * total_remaining / total_ratio))
else:
distributed = total_remaining
append(distributed)
total_ratio -= ratio
total_remaining -= distributed
return distributed_total
def ratio_reduce(
total: int, ratios: List[int], maximums: List[int], values: List[int]

View File

@ -58,8 +58,8 @@ if TYPE_CHECKING: # pragma: no cover
WINDOWS = platform.system() == "Windows"
HighlighterType = Callable[[Union[str, "Text"]], "Text"]
JustifyValues = Optional[Literal["left", "center", "right", "full"]]
OverflowValues = Literal["crop", "fold", "ellipsis"]
JustifyMethod = Literal["default", "left", "center", "right", "full"]
OverflowMethod = Literal["crop", "fold", "ellipsis"]
CONSOLE_HTML_FORMAT = """\
@ -91,8 +91,8 @@ class ConsoleOptions:
max_width: int
is_terminal: bool
encoding: str
justify: Optional[JustifyValues] = None
overflow: Optional[OverflowValues] = None
justify: Optional[JustifyMethod] = None
overflow: Optional[OverflowMethod] = None
no_wrap: bool = False
def update(
@ -100,8 +100,8 @@ class ConsoleOptions:
width: int = None,
min_width: int = None,
max_width: int = None,
justify: JustifyValues = None,
overflow: OverflowValues = None,
justify: JustifyMethod = None,
overflow: OverflowMethod = None,
no_wrap: bool = None,
) -> "ConsoleOptions":
"""Update values, return a copy."""
@ -557,7 +557,8 @@ class Console:
self,
text: str,
style: Union[str, Style] = "",
justify: JustifyValues = None,
justify: JustifyMethod = None,
overflow: OverflowMethod = None,
emoji: bool = None,
markup: bool = None,
highlighter: HighlighterType = None,
@ -568,7 +569,8 @@ class Console:
Args:
text (str): Text to render.
style (Union[str, Style], optional): Style to apply to rendered text.
justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to ``None``.
emoji (Optional[bool], optional): Enable emoji, or ``None`` to use Console default.
markup (Optional[bool], optional): Enable markup, or ``None`` to use Console default.
highlighter (HighlighterType, optional): Optional highlighter to apply.
@ -581,10 +583,13 @@ class Console:
if markup_enabled:
rich_text = render_markup(text, style=style, emoji=emoji_enabled)
rich_text.justify = justify
rich_text.overflow = overflow
else:
rich_text = Text(
_emoji_replace(text) if emoji_enabled else text,
justify=justify,
overflow=overflow,
style=style,
)
@ -626,7 +631,7 @@ class Console:
objects: Iterable[Any],
sep: str,
end: str,
justify: JustifyValues = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
@ -732,10 +737,12 @@ class Console:
sep=" ",
end="\n",
style: Union[str, Style] = None,
justify: JustifyValues = None,
justify: JustifyMethod = None,
overflow: OverflowMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
width: int = None,
) -> None:
r"""Print to the console.
@ -744,10 +751,12 @@ class Console:
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
justify (str, optional): Overflowmethod: "left", "right", "center", or "full". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to ``None``.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to ``None``.
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to ``None``.
width (Optional[int], optional): Width of output, or ``None`` to auto-detect. Defaults to ``None``.
"""
if not objects:
self.line()
@ -763,7 +772,9 @@ class Console:
markup=markup,
highlight=highlight,
)
render_options = self.options
render_options = self.options.update(
justify=justify, overflow=overflow, width=width
)
extend = self._buffer.extend
render = self.render
if style is None:
@ -804,7 +815,7 @@ class Console:
*objects: Any,
sep=" ",
end="\n",
justify: JustifyValues = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,

View File

@ -19,7 +19,8 @@ if TYPE_CHECKING:
Console,
ConsoleOptions,
ConsoleRenderable,
OverflowValues,
JustifyMethod,
OverflowMethod,
RenderResult,
RenderableType,
)
@ -110,32 +111,35 @@ class Lines:
self,
console: "Console",
width: int,
align: Literal["none", "left", "center", "right", "full"] = "left",
overflow: Optional["OverflowValues"] = None,
justify: "JustifyMethod" = "left",
overflow: "OverflowMethod" = "fold",
) -> None:
"""Pad each line with spaces to a given width.
"""Justify and overflow text to a given width.
Args:
console (Console): Console instance.
width (int): Number of characters per line.
justify (str, optional): Default justify method for text: "left", "center", "full" or "right". Defaults to "left".
overflow (str, optional): Default overflow for text: "crop", "fold", or "ellipisis". Defaults to "fold".
"""
from .text import Text
if align == "left":
if justify == "left":
for line in self._lines:
line.truncate(width, overflow=overflow, pad=True)
elif align == "center":
elif justify == "center":
for line in self._lines:
line.rstrip()
line.truncate(width, overflow=overflow)
line.pad_right((width - cell_len(line.plain)) // 2)
line.pad_left(width - cell_len(line.plain))
elif align == "right":
line.pad_left((width - cell_len(line.plain)) // 2)
line.pad_right(width - cell_len(line.plain))
elif justify == "right":
for line in self._lines:
line.rstrip()
line.truncate(width, overflow=overflow)
line.pad_left(width - cell_len(line.plain))
elif align == "full":
elif justify == "full":
for line_index, line in enumerate(self._lines):
if line_index == len(self._lines) - 1:
break

View File

@ -10,7 +10,7 @@ from .console import (
Console,
ConsoleOptions,
ConsoleRenderable,
JustifyValues,
JustifyMethod,
RenderResult,
Segment,
)
@ -113,13 +113,13 @@ class Paragraph(TextElement):
"""A Paragraph."""
style_name = "markdown.paragraph"
justify: JustifyValues
justify: JustifyMethod
@classmethod
def create(cls, markdown: "Markdown", node) -> "Paragraph":
return cls(justify=markdown.justify or "left")
def __init__(self, justify: JustifyValues) -> None:
def __init__(self, justify: JustifyMethod) -> None:
self.justify = justify
def __rich_console__(
@ -382,7 +382,7 @@ class Markdown(JupyterMixin):
Args:
markup (str): A string containing markdown.
code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai".
justify (JustifyValues, optional): Justify value for paragraphs. Defaults to None.
justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.
style (Union[str, Style], optional): Optional style to apply to markdown.
hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
"""
@ -403,7 +403,7 @@ class Markdown(JupyterMixin):
self,
markup: str,
code_theme: str = "monokai",
justify: JustifyValues = None,
justify: JustifyMethod = None,
style: Union[str, Style] = "none",
hyperlinks: bool = True,
) -> None:

View File

@ -27,7 +27,7 @@ from typing import (
from . import get_console
from .bar import Bar
from .console import Console, JustifyValues, RenderGroup, RenderableType
from .console import Console, JustifyMethod, RenderGroup, RenderableType
from .highlighter import Highlighter
from . import filesize
from .live_render import LiveRender
@ -129,7 +129,7 @@ class TextColumn(ProgressColumn):
self,
text_format: str,
style: StyleType = "none",
justify: JustifyValues = "left",
justify: JustifyMethod = "left",
markup: bool = True,
highlighter: Highlighter = None,
) -> None:
@ -688,7 +688,7 @@ class Progress:
sep=" ",
end="\n",
style: Union[str, Style] = None,
justify: JustifyValues = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
@ -716,7 +716,7 @@ class Progress:
*objects: Any,
sep=" ",
end="\n",
justify: JustifyValues = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,

View File

@ -29,7 +29,8 @@ if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
JustifyValues,
JustifyMethod,
OverflowMethod,
RenderableType,
RenderResult,
)
@ -58,10 +59,10 @@ class Column:
style: StyleType = "none"
"""StyleType: The style of the column."""
justify: "JustifyValues" = "left"
justify: "JustifyMethod" = "left"
"""str: How to justify text within the column ("left", "center", "right", or "full")"""
overflow: "OverflowValues" = "ellipsis"
overflow: "OverflowMethod" = "ellipsis"
width: Optional[int] = None
"""Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
@ -230,8 +231,12 @@ class Table(JupyterMixin):
table_width = (
sum(self._calculate_column_widths(console, max_width)) + extra_width
)
return Measurement(1, table_width)
_measure_column = self._measure_column
minimum_width = max(
_measure_column(console, column, max_width).minimum
for column in self.columns
)
return Measurement(minimum_width, table_width)
@property
def padding(self) -> Tuple[int, int, int, int]:
@ -251,8 +256,8 @@ class Table(JupyterMixin):
header_style: StyleType = None,
footer_style: StyleType = None,
style: StyleType = None,
justify: "JustifyValues" = "left",
overflow: "OverflowValues" = "ellipsis",
justify: "JustifyMethod" = "left",
overflow: "OverflowMethod" = "ellipsis",
width: int = None,
ratio: int = None,
no_wrap: bool = False,
@ -267,7 +272,7 @@ class Table(JupyterMixin):
header_style (Union[str, Style], optional): Style for the header. Defaults to "none".
footer_style (Union[str, Style], optional): Style for the header. Defaults to "none".
style (Union[str, Style], optional): Style for the column cells. Defaults to "none".
justify (JustifyValues, optional): Alignment for cells. Defaults to "left".
justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
width (int, optional): A minimum width in characters. Defaults to None.
ratio (int, optional): Flexible ratio for the column. Defaults to None.
no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
@ -398,6 +403,7 @@ class Table(JupyterMixin):
widths[index] = fixed_widths[index] + next(iter_flex_widths)
table_width = sum(widths)
# Reduce rows that not no_wrap
if table_width > max_width:
excess_width = table_width - max_width
widths = ratio_reduce(
@ -408,6 +414,7 @@ class Table(JupyterMixin):
)
table_width = sum(widths)
# Reduce rows that are no_wrap
if table_width > max_width:
excess_width = table_width - max_width
widths = ratio_reduce(
@ -425,54 +432,6 @@ class Table(JupyterMixin):
)
table_width = sum(widths)
# flex_widths = [_range.span for _range in width_ranges]
# excess_width = table_width - max_width
# widths = [
# max(width_range.minimum + 2, width - excess_width)
# for width_range, width, excess_width in zip(
# width_ranges, widths, ratio_divide(excess_width, flex_widths)
# )
# ]
# flex_widths = [
# 0 if column.no_wrap else 1
# for width_range, column in zip(width_ranges, columns)
# ]
# if any(flex_widths):
# widths = [
# max(width_range.minimum, width - excess_width)
# for width_range, width, excess_width in zip(
# width_ranges,
# widths,
# ratio_divide(excess_width, flex_widths),
# )
# ]
table_width = sum(widths)
# if table_width > max_width:
# excess_width = table_width - max_width
# flex_widths = [0 if column.no_wrap else 1 for column in columns]
# if any(flex_widths):
# widths = [
# max(width_range.minimum, width - excess_width)
# for width_range, width, excess_width in zip(
# width_ranges,
# widths,
# ratio_divide(excess_width, flex_widths),
# )
# ]
# table_width = sum(widths)
# if table_width > max_width:
# flex_widths = [1 for column in columns]
# excess_width = table_width - max_width
# widths = [
# width - excess_width
# for width_range, width, excess_width in zip(
# width_ranges, widths, ratio_divide(excess_width, flex_widths),
# )
# ]
elif table_width < max_width and self.expand:
pad_widths = ratio_divide(max_width - table_width, widths)
widths = [_width + pad for _width, pad in zip(widths, pad_widths)]

View File

@ -30,13 +30,17 @@ if TYPE_CHECKING: # pragma: no cover
from .console import (
Console,
ConsoleOptions,
JustifyValues,
OverflowValues,
JustifyMethod,
OverflowMethod,
RenderResult,
RenderableType,
)
DEFAULT_OVERFLOW = "fold"
DEFAULT_JUSTIFY: "JustifyMethod" = "default"
DEFAULT_OVERFLOW: "OverflowMethod" = "fold"
_re_whitespace = re.compile(r"\s+$")
class Span(NamedTuple):
@ -98,7 +102,8 @@ class Text(JupyterMixin):
Args:
text (str, optional): Default unstyled text. Defaults to "".
style (Union[str, Style], optional): Base style for text. Defaults to "".
justify (str, optional): Default alignment for text, "left", "center", "full" or "right". Defaults to None.
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
end (str, optional): Character to end text with. Defaults to "\n".
tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
"""
@ -107,8 +112,8 @@ class Text(JupyterMixin):
self,
text: str = "",
style: Union[str, Style] = "",
justify: "JustifyValues" = None,
overflow: "OverflowValues" = None,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
no_wrap: bool = False,
end: str = "\n",
tab_size: Optional[int] = 8,
@ -162,14 +167,16 @@ class Text(JupyterMixin):
text: str,
style: Union[str, Style] = "",
emoji: bool = True,
justify: "JustifyValues" = None,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
) -> "Text":
"""Create Text instance from markup.
Args:
text (str): A string containing console markup.
emoji (bool, optional): Also render emoji code. Defaults to True.
justify (str, optional): Default alignment for text, "left", "center", "full" or "right". Defaults to None.
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
Returns:
Text: A Text instance with markup rendered.
@ -178,6 +185,7 @@ class Text(JupyterMixin):
rendered_text = render(text, style, emoji=emoji)
rendered_text.justify = justify
rendered_text.overflow = overflow
return rendered_text
@classmethod
@ -185,7 +193,8 @@ class Text(JupyterMixin):
cls,
*parts: Union[str, "Text", Tuple[str, StyleType]],
style: Union[str, Style] = "",
justify: "JustifyValues" = None,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
end: str = "\n",
tab_size: int = 8,
) -> "Text":
@ -194,14 +203,17 @@ class Text(JupyterMixin):
Args:
style (Union[str, Style], optional): Base style for text. Defaults to "".
justify (str, optional): Default alignment for text, "left", "center", "full" or "right". Defaults to None.
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.
overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.
end (str, optional): Character to end text with. Defaults to "\n".
tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to 8.
Returns:
Text: A new text instance.
"""
text = cls(style=style, justify=justify, end=end, tab_size=tab_size)
text = cls(
style=style, justify=justify, overflow=overflow, end=end, tab_size=tab_size
)
append = text.append
for part in parts:
if isinstance(part, (Text, str)):
@ -221,11 +233,12 @@ class Text(JupyterMixin):
@plain.setter
def plain(self, new_text: str) -> None:
"""Set the text to a new value."""
self._text[:] = [new_text]
old_length = self._length
self._length = len(new_text)
if old_length > self._length:
self._trim_spans()
if new_text != self.plain:
self._text[:] = [new_text]
old_length = self._length
self._length = len(new_text)
if old_length > self._length:
self._trim_spans()
@property
def spans(self) -> List[Span]:
@ -253,7 +266,7 @@ class Text(JupyterMixin):
"""Return a copy of this instance."""
copy_self = Text(
self.plain,
style=self.style.copy() if isinstance(self.style, Style) else self.style,
style=self.style,
justify=self.justify,
overflow=self.overflow,
no_wrap=self.no_wrap,
@ -369,6 +382,20 @@ class Text(JupyterMixin):
"""Trip whitespace from end of text."""
self.plain = self.plain.rstrip()
def rstrip_end(self, size: int):
"""Remove whitespace beyond a certain width at the end of the text.
Args:
size (int): The desired size of the text.
"""
text_length = len(self)
if text_length > size:
excess = text_length - size
whitespace_match = _re_whitespace.search(self.plain)
if whitespace_match is not None:
whitespace_count = len(whitespace_match.group(0))
self.plain = self.plain[: -min(whitespace_count, excess)]
def set_length(self, new_length: int) -> None:
"""Set new length of the text, clipping or padding is required."""
length = len(self)
@ -389,10 +416,13 @@ class Text(JupyterMixin):
self, console: "Console", options: "ConsoleOptions"
) -> Iterable[Segment]:
tab_size: int = console.tab_size or self.tab_size or 8 # type: ignore
overflow = cast(
"OverflowValues", self.overflow or options.overflow or DEFAULT_OVERFLOW
justify = cast(
"JustifyMethod", self.justify or options.justify or DEFAULT_OVERFLOW
)
justify = self.justify or options.justify
overflow = cast(
"OverflowMethod", self.overflow or options.overflow or DEFAULT_OVERFLOW
)
if self.no_wrap or options.no_wrap:
render_text = self
if overflow in ("crop", "ellipsis"):
@ -401,7 +431,7 @@ class Text(JupyterMixin):
if justify:
lines = Lines([render_text])
lines.justify(
console, options.max_width, align=justify, overflow=overflow
console, options.max_width, justify=justify, overflow=overflow
)
render_text = lines[0]
yield from render_text.render(console, end=self.end)
@ -413,13 +443,6 @@ class Text(JupyterMixin):
overflow=overflow,
tab_size=tab_size or 8,
)
# new_line = Segment.line()
# for last, line in loop_last(lines):
# yield from line.render(console)
# if not last:
# yield new_line
# else:
# yield Segment(self.end)
all_lines = Text("\n").join(lines)
yield from all_lines.render(console, end=self.end)
@ -544,20 +567,20 @@ class Text(JupyterMixin):
def truncate(
self,
max_width: int,
overflow: Optional["OverflowValues"] = None,
overflow: Optional["OverflowMethod"] = None,
pad: bool = False,
) -> None:
"""Truncate text if it is longer that a given width.
Args:
max_width (int): Maximum number of characters in text.
ellipsis (bool): Replace last character with ellipsis if truncated.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None, to use self.overflow.
pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.
"""
length = cell_len(self.plain)
_overflow = overflow or self.overflow or DEFAULT_OVERFLOW
if length > max_width:
overflow = overflow or self.overflow or DEFAULT_OVERFLOW
if overflow == "ellipsis":
if _overflow == "ellipsis":
self.plain = set_cell_size(self.plain, max_width - 1).rstrip() + ""
else:
self.plain = set_cell_size(self.plain, max_width)
@ -571,13 +594,18 @@ class Text(JupyterMixin):
new_length = self._length
spans: List[Span] = []
append = spans.append
_Span = Span
for span in self._spans:
if span.end < new_length:
append(span)
continue
if span.start >= new_length:
continue
append(span.right_crop(new_length))
if span.end > new_length:
start, end, style = span
append(_Span(start, min(new_length, end), style))
else:
append(span)
self._spans[:] = spans
def pad_left(self, count: int, character: str = " ") -> None:
@ -741,7 +769,7 @@ class Text(JupyterMixin):
if new_span is None:
break
span = new_span
line_index += 1
line_index = (line_index + 1) % len(line_ranges)
line_start, line_end = line_ranges[line_index]
return new_lines
@ -754,21 +782,26 @@ class Text(JupyterMixin):
self,
console: "Console",
width: int,
justify: Optional["JustifyValues"] = "left",
overflow: Optional["OverflowValues"] = None,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
tab_size: int = 8,
) -> Lines:
"""Word wrap the text.
Args:
console (Console): Console instance.
width (int): Number of characters per line.
justify (bool, optional): True to pad lines with spaces. Defaults to False.
emoji (bool, optional): Also render emoji code. Defaults to True.
justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to "left".
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None.
tab_size (int, optional): Default tab size. Defaults to 8.
Returns:
Lines: Number of lines.
"""
wrap_overflow: "OverflowValues" = cast(
"OverflowValues", overflow or self.overflow or DEFAULT_OVERFLOW
wrap_justify = cast("JustifyMethod", justify or self.justify or DEFAULT_JUSTIFY)
wrap_overflow = cast(
"OverflowMethod", overflow or self.overflow or DEFAULT_OVERFLOW
)
lines: Lines = Lines()
for line in self.split():
@ -776,13 +809,16 @@ class Text(JupyterMixin):
line = line.tabs_to_spaces(tab_size)
offsets = divide_line(str(line), width, fold=wrap_overflow == "fold")
new_lines = line.divide(offsets)
if justify:
new_lines.justify(console, width, align=justify, overflow=overflow)
for line in new_lines:
line.rstrip_end(width)
if wrap_justify:
new_lines.justify(
console, width, justify=wrap_justify, overflow=wrap_overflow
)
lines.extend(new_lines)
if wrap_overflow in ("crop", "ellipsis"):
for line in lines:
line.truncate(width, wrap_overflow)
for line in lines:
line.truncate(width, wrap_overflow)
return lines
@ -801,3 +837,11 @@ class Text(JupyterMixin):
line.set_length(width)
append(line)
return lines
if __name__ == "__main__":
from rich.console import Console
console = Console()
t = Text("foo bar", justify="left")
print(repr(t.wrap(console, 4)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,10 +23,11 @@ def render_log():
def test_log():
expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b[2mtest_log.py:20\x1b[0m\x1b[2m \x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b[2mtest_log.py:21\x1b[0m\x1b[2m \x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n"
expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b[2mtest_log.py:20\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b[2mtest_log.py:21\x1b[0m\n \x1b[3m Locals \x1b[0m \n \x1b[34m╭─────────┬────────────────────────────────────────╮\x1b[0m \n \x1b[34m│\x1b[0m\x1b[32m'console'\x1b[0m\x1b[34m│\x1b[0m\x1b[1m<\x1b[0m\x1b[1;38;5;13mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m\x1b[34m│\x1b[0m \n \x1b[34m╰─────────┴────────────────────────────────────────╯\x1b[0m \n"
assert render_log() == expected
if __name__ == "__main__":
print(render_log())
print(repr(render_log()))
render = render_log()
print(render)
print(repr(render))

View File

@ -21,7 +21,7 @@ def make_log():
def test_log():
render = make_log()
expected = "\x1b[2;36m[DATE]\x1b[0m\x1b[2;36m \x1b[0m\x1b[32mDEBUG\x1b[0m foo \x1b[2mtest_logging.py:17\x1b[0m\x1b[2m \x1b[0m\n"
expected = "\x1b[2;36m[DATE]\x1b[0m\x1b[2;36m \x1b[0m\x1b[32mDEBUG\x1b[0m foo \x1b[2mtest_logging.py:17\x1b[0m\n"
assert render == expected

View File

@ -145,7 +145,7 @@ def test_track() -> None:
assert value == next(expected_values)
result = console.file.getvalue()
print(repr(result))
expected = "test \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[?25l\r\x1b[2Ktest \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━\x1b[0m \x1b[35m 33%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[2Ktest \x1b[38;2;249;38;114m━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━\x1b[0m \x1b[35m 67%\x1b[0m \x1b[36m0:00:06\x1b[0m \r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m \r\x1b[2Ktest \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m \n\x1b[?25h"
expected = " \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-…\x1b[0m \x1b[?25l\r\x1b[2K \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-…\x1b[0m \r\x1b[2K\x1b[38;2;249;38;114m━━━━━━━━━━━\x1b[0m\x1b[38;2;249;38;114m╸\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:…\x1b[0m \r\x1b[2K\x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━\x1b[0m \x1b[36m0:…\x1b[0m \r\x1b[2K\x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:…\x1b[0m \r\x1b[2K\x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:…\x1b[0m \n\x1b[?25h"
assert result == expected
with pytest.raises(ValueError):
@ -192,7 +192,7 @@ def test_columns() -> None:
progress.refresh()
result = console.file.getvalue()
print(repr(result))
expected = 'test foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \r\x1b[2Ktest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[?25l\r\x1b[1A\x1b[2Ktest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \x1b[2mtest_progress.py:190\x1b[0m\x1b[2m \x1b[0m\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m \r\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m \n\x1b[?25h'
expected = "test foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \r\x1b[2Ktest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[?25l\r\x1b[1A\x1b[2Ktest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \x1b[2mtest_progress.py:190\x1b[0m\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \r\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m \r\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m12 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m12/10 bytes\x1b[0m \x1b[31m1 byte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[32m16 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m16/7 bytes \x1b[0m \x1b[31m2 bytes/s\x1b[0m \n\x1b[?25h"
assert result == expected

View File

@ -313,8 +313,18 @@ def test_right_crop():
assert test._spans == [Span(0, 3, "red")]
def test_wrap_4():
def test_wrap_3():
test = Text("foo bar baz")
lines = test.wrap(Console(), 3)
print(repr(lines))
assert len(lines) == 3
assert lines[0] == Text("foo")
assert lines[1] == Text("bar")
assert lines[2] == Text("baz")
def test_wrap_4():
test = Text("foo bar baz", justify="left")
lines = test.wrap(Console(), 4)
assert len(lines) == 3
assert lines[0] == Text("foo ")
@ -322,17 +332,8 @@ def test_wrap_4():
assert lines[2] == Text("baz ")
def test_wrap_3():
test = Text("foo bar baz")
lines = test.wrap(Console(), 3)
assert len(lines) == 3
assert lines[0] == Text("foo")
assert lines[1] == Text("bar")
assert lines[2] == Text("baz")
def test_wrap_long():
test = Text("abracadabra")
test = Text("abracadabra", justify="left")
lines = test.wrap(Console(), 4)
assert len(lines) == 3
assert lines[0] == Text("abra")
@ -341,7 +342,7 @@ def test_wrap_long():
def test_wrap_long_words():
test = Text("X 123456789")
test = Text("X 123456789", justify="left")
lines = test.wrap(Console(), 4)
assert len(lines) == 3
@ -358,7 +359,7 @@ def test_fit():
def test_wrap_tabs():
test = Text("foo\tbar",)
test = Text("foo\tbar", justify="left")
lines = test.wrap(Console(), 4)
assert len(lines) == 2
assert str(lines[0]) == "foo "