Merge pull request #102 from willmcgugan/ellipsis

overflow
This commit is contained in:
Will McGugan 2020-06-07 10:11:31 +01:00 committed by GitHub
commit 72802723f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 627 additions and 205 deletions

View file

@ -5,6 +5,28 @@ 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).
## [2.0.0] - Unreleased
### Added
- Added overflow methods
- Added no_wrap option to print()
- Added width option to print
- Improved handling of compressed tables
### Fixed
- Fixed erroneous space at end of log
- Fixed erroneous space at end of progress bar
### Changed
- Renamed \_ratio.ratio_divide to \_ratio.ratio_distribute
- Renamed JustifyValues to JustifyMethod
- Optimized \_trim_spans
- Enforced keyword args in Console / Text interfaces
- Return self from text.append
## [1.3.1] - 2020-06-01
### Changed

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.
The [Rich API](https://rich.readthedocs.io/en/latest/) makes it easy to add color and style to terminal output. Rich can also render pretty tables, progress bars, markdown, syntax highlighted source code, tracebacks, and more — 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.

View file

@ -88,12 +88,13 @@ The :meth:`~rich.console.Console.log` methods offers the same capabilities as pr
To help with debugging, the log() method has a ``log_locals`` parameter. If you set this to ``True``, Rich will display a table of local variables where the method was called.
Justify / Alignment
-------------------
Both print and log support a ``justify`` argument which if set must be one of "left", "right", "center", or "full". If "left", any text printed (or logged) will be left aligned, if "right" text will be aligned to the right of the terminal, if "center" the text will be centered, and if "full" the text will be lined up with both the left and right edges of the terminal (like printed text in a book).
Both print and log support a ``justify`` argument which if set must be one of "default", "left", "right", "center", or "full". If "left", any text printed (or logged) will be left aligned, if "right" text will be aligned to the right of the terminal, if "center" the text will be centered, and if "full" the text will be lined up with both the left and right edges of the terminal (like printed text in a book).
The default for ``justify`` is ``None`` which will generally look the same as ``"left"`` but with a subtle difference. Left justify will pad the right of the text with spaces, while a None justify will not. You will only notice the difference if you set a background color with the ``style`` argument. The following example demonstrates the difference::
The default for ``justify`` is ``"default"`` which will generally look the same as ``"left"`` but with a subtle difference. Left justify will pad the right of the text with spaces, while a default justify will not. You will only notice the difference if you set a background color with the ``style`` argument. The following example demonstrates the difference::
from rich.console import Console
@ -116,6 +117,48 @@ This produces the following output:
Rich
</span></pre>
Overflow
--------
Overflow is what happens when text you print is larger than the available space. Overflow may occur if you print long 'words' such as URLs for instance, or if you have text inside a panel or table cell with restricted space.
You can specify how Rich should handle overflow with the ``overflow`` argument to :meth:`~rich.console.Console.print` which should be one of the following strings: "fold", "crop", or "ellipsis". The default is "fold" which will put any excess characters on the following line, creating as many new lines as required to fit the text.
The "crop" method truncates the text at the end of the line, discarding any characters that would overflow.
The "ellipsis" method is similar to "crop", but will insert an ellipsis character ("…") at the end of any text that has been truncated.
You can specify which overflow method to use with the ``overflow`` argument to :meth:`~rich.console.Console.print`. The following code demonstrates Rich's overflow methods::
from typing import List
from rich.console import Console, OverflowMethod
console = Console(width=14)
supercali = "supercalifragilisticexpialidocious"
overflow_methods: List[OverflowMethod] = ["fold", "crop", "ellipsis"]
for overflow in overflow_methods:
console.rule(overflow)
console.print(supercali, overflow=overflow, style="bold blue")
console.print()
This produces the following output:
.. raw:: html
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #00ff00">──── </span>fold<span style="color: #00ff00"> ────</span>
<span style="color: #000080; font-weight: bold">supercalifragi
listicexpialid
ocious
</span>
<span style="color: #00ff00">──── </span>crop<span style="color: #00ff00"> ────</span>
<span style="color: #000080; font-weight: bold">supercalifragi
</span>
<span style="color: #00ff00">── </span>ellipsis<span style="color: #00ff00"> ──</span>
<span style="color: #000080; font-weight: bold">supercalifrag…
</span>
</pre>
Input
-----

View file

@ -27,7 +27,7 @@ else:
return text
filenames = [
filename for filename in os.listdir(sys.argv[1]) if not filename.startswith(".")
filename for filename in os.listdir(root_path) if not filename.startswith(".")
]
filenames.sort(key=lambda filename: filename.lower())
filename_text = [make_filename_text(filename) for filename in filenames]

11
examples/overflow.py Normal file
View file

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

View file

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

View file

@ -36,12 +36,13 @@ class LogRender:
from .containers import Renderables
from .table import Table
output = Table(show_header=False, expand=True, box=None, padding=(0, 1, 0, 0))
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
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"] = []

17
rich/_pick.py Normal file
View file

@ -0,0 +1,17 @@
from typing import Optional
def pick_bool(*values: Optional[bool]) -> bool:
"""Pick the first non-none bool or return the last value.
Args:
*values (bool): Any number of boolean or None values.
Returns:
bool: First non-none boolean.
"""
assert values, "1 or more values required"
for value in values:
if value is not None:
return value
return bool(value)

View file

@ -1,19 +1,54 @@
from dataclasses import dataclass
from math import ceil
from typing import List
def ratio_divide(
total: int, ratios: List[int], minimums: List[int] = None
def ratio_reduce(
total: int, ratios: List[int], maximums: List[int], values: 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 rations.
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.
"""
ratios = [ratio if _max else 0 for ratio, _max in zip(ratios, maximums)]
total_ratio = sum(ratios)
if not total_ratio:
return values[:]
total_remaining = total
result: List[int] = []
append = result.append
for ratio, maximum, value in zip(ratios, maximums, values):
if ratio and total_ratio > 0:
distributed = min(maximum, round(ratio * total_remaining / total_ratio))
append(value - distributed)
total_remaining -= distributed
total_ratio -= ratio
else:
append(value)
return result
def ratio_distribute(
total: int, ratios: List[int], minimums: List[int] = None
) -> List[int]:
"""Distribute 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.
"""
if minimums:
ratios = [ratio if _min else 0 for ratio, _min in zip(ratios, minimums)]
total_ratio = sum(ratios)
assert total_ratio > 0, "Sum of ratios must be > 0"
@ -26,7 +61,7 @@ def ratio_divide(
_minimums = minimums
for ratio, minimum in zip(ratios, _minimums):
if total_ratio > 0:
distributed = max(minimum, round(ratio * total_remaining / total_ratio))
distributed = max(minimum, ceil(ratio * total_remaining / total_ratio))
else:
distributed = total_remaining
append(distributed)

View file

@ -17,22 +17,26 @@ def words(text: str) -> Iterable[Tuple[int, int, str]]:
word_match = re_word.match(text, end)
def divide_line(text: str, width: int) -> List[int]:
def divide_line(text: str, width: int, fold: bool = True) -> List[int]:
divides: List[int] = []
append = divides.append
line_position = 0
for start, _end, word in words(text):
word_length = cell_len(word.rstrip())
if line_position + word_length > width:
if word_length > width:
for last, line in loop_last(
chop_cells(word, width, position=line_position)
):
if last:
line_position = cell_len(line)
else:
start += len(line)
append(start)
if fold:
for last, line in loop_last(
chop_cells(word, width, position=line_position)
):
if last:
line_position = cell_len(line)
else:
start += len(line)
append(start)
else:
line_position = cell_len(word)
elif line_position and start:
append(start)
line_position = cell_len(word)

View file

@ -21,34 +21,36 @@ class Box:
def __init__(self, box: str) -> None:
self._box = box
(line1, line2, line3, line4, line5, line6, line7, line8,) = box.splitlines()
line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
# top
self.top_left, self.top, self.top_divider, self.top_right = line1
self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
# head
self.head_left, _, self.head_vertical, self.head_right = line2
self.head_left, _, self.head_vertical, self.head_right = iter(line2)
# head_row
(
self.head_row_left,
self.head_row_horizontal,
self.head_row_cross,
self.head_row_right,
) = line3
) = iter(line3)
# mid
self.mid_left, _, self.mid_vertical, self.mid_right = line4
self.mid_left, _, self.mid_vertical, self.mid_right = iter(line4)
# row
self.row_left, self.row_horizontal, self.row_cross, self.row_right = line5
self.row_left, self.row_horizontal, self.row_cross, self.row_right = iter(line5)
# foot_row
(
self.foot_row_left,
self.foot_row_horizontal,
self.foot_row_cross,
self.foot_row_right,
) = line6
) = iter(line6)
# foot
self.foot_left, _, self.foot_vertical, self.foot_right = line7
self.foot_left, _, self.foot_vertical, self.foot_right = iter(line7)
# bottom
self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = line8
self.bottom_left, self.bottom, self.bottom_divider, self.bottom_right = iter(
line8
)
def __repr__(self) -> str:
return "Box(...)"

View file

@ -490,7 +490,7 @@ if __name__ == "__main__": # pragma: no cover
console = Console()
table = Table(show_footer=False, show_edge=True)
table.add_column("Color", width=10)
table.add_column("Color", width=10, overflow="ellipsis")
table.add_column("Number", justify="right", style="yellow")
table.add_column("Name", style="green")
table.add_column("Hex", style="blue")

View file

@ -58,7 +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"]]
JustifyMethod = Literal["default", "left", "center", "right", "full"]
OverflowMethod = Literal["fold", "crop", "ellipsis"]
CONSOLE_HTML_FORMAT = """\
@ -90,14 +91,18 @@ class ConsoleOptions:
max_width: int
is_terminal: bool
encoding: str
justify: Optional[JustifyValues] = None
justify: Optional[JustifyMethod] = None
overflow: Optional[OverflowMethod] = None
no_wrap: Optional[bool] = False
def update(
self,
width: int = None,
min_width: int = None,
max_width: int = None,
justify: JustifyValues = None,
justify: JustifyMethod = None,
overflow: OverflowMethod = None,
no_wrap: bool = None,
) -> "ConsoleOptions":
"""Update values, return a copy."""
options = replace(self)
@ -109,6 +114,10 @@ class ConsoleOptions:
options.max_width = max_width
if justify is not None:
options.justify = justify
if overflow is not None:
options.overflow = overflow
if no_wrap is not None:
options.no_wrap = no_wrap
return options
@ -130,11 +139,11 @@ class ConsoleRenderable(Protocol):
...
"""A type that may be rendered by Console."""
RenderableType = Union[ConsoleRenderable, RichCast, str]
"""A type that may be rendered by Console."""
"""The result of calling a __rich_console__ method."""
RenderResult = Iterable[Union[RenderableType, Segment]]
"""The result of calling a __rich_console__ method."""
_null_highlighter = NullHighlighter()
@ -469,7 +478,7 @@ class Console:
self.control("\033[?25h" if show else "\033[?25l")
def render(
self, renderable: RenderableType, options: ConsoleOptions,
self, renderable: RenderableType, options: ConsoleOptions
) -> Iterable[Segment]:
"""Render an object in to an iterable of `Segment` instances.
@ -512,6 +521,7 @@ class Console:
self,
renderable: RenderableType,
options: Optional[ConsoleOptions],
*,
style: Optional[Style] = None,
pad: bool = True,
) -> List[List[Segment]]:
@ -547,8 +557,10 @@ class Console:
def render_str(
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,
@ -559,7 +571,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: "default", "left", "center", "full", or "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.
@ -572,10 +585,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,
)
@ -617,7 +633,8 @@ class Console:
objects: Iterable[Any],
sep: str,
end: str,
justify: JustifyValues = None,
*,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
@ -658,7 +675,7 @@ class Console:
def check_text() -> None:
if text:
sep_text = Text(sep, justify=text[0].justify or justify, end=end)
sep_text = Text(sep, end=end)
append(sep_text.join(text))
del text[:]
@ -670,14 +687,11 @@ class Console:
append_text(
self.render_str(
renderable,
justify=justify,
emoji=emoji,
markup=markup,
highlighter=_highlighter,
)
)
elif isinstance(renderable, Text):
append_text(renderable)
elif isinstance(renderable, ConsoleRenderable):
check_text()
append(renderable)
@ -693,6 +707,7 @@ class Console:
def rule(
self,
title: str = "",
*,
character: str = "",
style: Union[str, Style] = "rule.line",
) -> None:
@ -723,10 +738,13 @@ class Console:
sep=" ",
end="\n",
style: Union[str, Style] = None,
justify: JustifyValues = None,
justify: JustifyMethod = None,
overflow: OverflowMethod = None,
no_wrap: bool = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
width: int = None,
) -> None:
r"""Print to the console.
@ -735,10 +753,13 @@ 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): Justify method: "default", "left", "right", "center", or "full". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipisis". Defaults to None.
no_wrap (Optional[bool], optional): Disable word wrapping. 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()
@ -754,7 +775,9 @@ class Console:
markup=markup,
highlight=highlight,
)
render_options = self.options
render_options = self.options.update(
justify=justify, overflow=overflow, width=width, no_wrap=no_wrap
)
extend = self._buffer.extend
render = self.render
if style is None:
@ -770,6 +793,7 @@ class Console:
def print_exception(
self,
*,
width: Optional[int] = 88,
extra_lines: int = 3,
theme: Optional[str] = None,
@ -795,7 +819,7 @@ class Console:
*objects: Any,
sep=" ",
end="\n",
justify: JustifyValues = None,
justify: JustifyMethod = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
@ -901,7 +925,7 @@ class Console:
result = input()
return result
def export_text(self, clear: bool = True, styles: bool = False) -> str:
def export_text(self, *, clear: bool = True, styles: bool = False) -> str:
"""Generate text from console contents (requires record=True argument in constructor).
Args:
@ -929,7 +953,7 @@ class Console:
del self._record_buffer[:]
return text
def save_text(self, path: str, clear: bool = True, styles: bool = False) -> None:
def save_text(self, path: str, *, clear: bool = True, styles: bool = False) -> None:
"""Generate text from console and save to a given location (requires record=True argument in constructor).
Args:
@ -945,6 +969,7 @@ class Console:
def export_html(
self,
*,
theme: TerminalTheme = None,
clear: bool = True,
code_format: str = None,
@ -1024,6 +1049,7 @@ class Console:
def save_html(
self,
path: str,
*,
theme: TerminalTheme = None,
clear: bool = True,
code_format=CONSOLE_HTML_FORMAT,

View file

@ -1,5 +1,14 @@
from itertools import zip_longest
from typing import Iterator, Iterable, List, overload, TypeVar, TYPE_CHECKING, Union
from typing import (
Iterator,
Iterable,
List,
Optional,
overload,
TypeVar,
TYPE_CHECKING,
Union,
)
from typing_extensions import Literal
from .segment import Segment
@ -10,6 +19,8 @@ if TYPE_CHECKING:
Console,
ConsoleOptions,
ConsoleRenderable,
JustifyMethod,
OverflowMethod,
RenderResult,
RenderableType,
)
@ -100,29 +111,35 @@ class Lines:
self,
console: "Console",
width: int,
align: Literal["none", "left", "center", "right", "full"] = "left",
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.set_length(width)
elif align == "center":
line.truncate(width, overflow=overflow, pad=True)
elif justify == "center":
for line in self._lines:
line.rstrip()
line.truncate(width, overflow=overflow)
line.pad_left((width - cell_len(line.plain)) // 2)
line.pad_right(width - cell_len(line.plain))
elif align == "right":
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

@ -34,7 +34,7 @@ class LiveRender:
self, console: Console, options: ConsoleOptions
) -> RenderResult:
style = console.get_style(self.style)
lines = console.render_lines(self.renderable, options, style, pad=False)
lines = console.render_lines(self.renderable, options, style=style, pad=False)
shape = Segment.get_shape(lines)
if self._shape is None:

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:
@ -609,9 +609,7 @@ class Progress:
Table: A table instance.
"""
table = Table.grid()
table.pad_edge = True
table.padding = (0, 1, 0, 0)
table = Table.grid(padding=(0, 1))
for _ in self.columns:
table.add_column()
for task in tasks:
@ -688,7 +686,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 +714,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

@ -211,8 +211,8 @@ class Syntax(JupyterMixin):
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
if self.code_width is not None:
width = self.code_width + self._numbers_column_width
return Measurement(width, width)
return Measurement(max_width, max_width)
return Measurement(self._numbers_column_width, width)
return Measurement(self._numbers_column_width, max_width)
def __rich_console__(
self, console: Console, options: ConsoleOptions

View file

@ -16,7 +16,7 @@ from typing_extensions import Literal
from . import box, errors
from ._loop import loop_first, loop_first_last, loop_last
from ._ratio import ratio_divide
from ._ratio import ratio_distribute, ratio_reduce
from .jupyter import JupyterMixin
from .measure import Measurement
from .padding import Padding, PaddingDimensions
@ -29,7 +29,8 @@ if TYPE_CHECKING:
from .console import (
Console,
ConsoleOptions,
JustifyValues,
JustifyMethod,
OverflowMethod,
RenderableType,
RenderResult,
)
@ -40,6 +41,9 @@ if TYPE_CHECKING:
class Column:
"""Defines a column in a table."""
index: int
"""Index of column."""
header: "RenderableType" = ""
"""RenderableType: Renderable for the header (typically a string)"""
@ -55,9 +59,11 @@ 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: "OverflowMethod" = "ellipsis"
width: Optional[int] = None
"""Optional[int]: Width of the column, or ``None`` (default) to auto calculate width."""
@ -140,8 +146,8 @@ class Table(JupyterMixin):
) -> None:
self.columns = [
(Column(header) if isinstance(header, str) else header)
for header in headers
(Column(index, header) if isinstance(header, str) else header)
for index, header in enumerate(headers)
]
self.title = title
self.caption = caption
@ -225,11 +231,12 @@ class Table(JupyterMixin):
table_width = (
sum(self._calculate_column_widths(console, max_width)) + extra_width
)
min_table_width = (
sum(self._calculate_column_widths(console, max_width, minimums=True))
+ extra_width
_measure_column = self._measure_column
minimum_width = max(
_measure_column(console, column, max_width).minimum
for column in self.columns
)
return Measurement(min_table_width, table_width)
return Measurement(minimum_width, table_width)
@property
def padding(self) -> Tuple[int, int, int, int]:
@ -249,7 +256,8 @@ class Table(JupyterMixin):
header_style: StyleType = None,
footer_style: StyleType = None,
style: StyleType = None,
justify: "JustifyValues" = "left",
justify: "JustifyMethod" = "left",
overflow: "OverflowMethod" = "ellipsis",
width: int = None,
ratio: int = None,
no_wrap: bool = False,
@ -264,13 +272,14 @@ 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.
"""
column = Column(
index=len(self.columns),
header=header,
footer=footer,
header_style=Style.pick_first(
@ -281,6 +290,7 @@ class Table(JupyterMixin):
),
style=Style.pick_first(style, self.style, "table.cell"),
justify=justify,
overflow=overflow,
width=width,
ratio=ratio,
no_wrap=no_wrap,
@ -310,7 +320,7 @@ class Table(JupyterMixin):
]
for index, renderable in enumerate(cell_renderables):
if index == len(columns):
column = Column()
column = Column(index)
for _ in range(self._row_count):
add_cell(column, Text(""))
self.columns.append(column)
@ -347,7 +357,7 @@ class Table(JupyterMixin):
render_text = text
else:
render_text = console.render_str(text, style=style)
return render_text.wrap(console, table_width, "center")
return render_text.wrap(console, table_width, justify="center")
if self.title:
yield render_annotation(
@ -360,21 +370,16 @@ class Table(JupyterMixin):
style=Style.pick_first(self.caption_style, "table.caption"),
)
def _calculate_column_widths(
self, console: "Console", max_width: int, minimums: bool = False
) -> List[int]:
"""Calculate the widths of each column."""
def _calculate_column_widths(self, console: "Console", max_width: int) -> List[int]:
"""Calculate the widths of each column, including padding, not including borders."""
columns = self.columns
width_ranges = [
self._measure_column(console, column_index, column, max_width)
self._measure_column(console, column, max_width)
for column_index, column in enumerate(columns)
]
if minimums:
widths = [_range.minimum or 1 for _range in width_ranges]
else:
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
padding_width = self.padding[1] + self.padding[3]
if self.expand:
ratios = [col.ratio or 0 for col in columns if col.flexible]
if any(ratios):
@ -383,38 +388,54 @@ class Table(JupyterMixin):
for _range, column in zip(width_ranges, columns)
]
flex_minimum = [
(column.width or 1) + padding_width
(column.width or 1) + get_padding_width(column.index)
for column in columns
if column.flexible
]
flexible_width = max_width - sum(fixed_widths)
flex_widths = ratio_divide(flexible_width, ratios, flex_minimum)
flex_widths = ratio_distribute(flexible_width, ratios, flex_minimum)
iter_flex_widths = iter(flex_widths)
for index, column in enumerate(columns):
if column.flexible:
widths[index] = fixed_widths[index] + next(iter_flex_widths)
table_width = sum(widths)
# Reduce rows that not no_wrap
if table_width > max_width:
flex_widths = [_range.span for _range in width_ranges]
if not any(flex_widths):
flex_widths = [
0 if column.no_wrap else width
for column, width in zip(columns, widths)
]
if not any(flex_widths):
return widths
excess_width = table_width - max_width
widths = [
max(1 + padding_width, width - excess_width)
for width, excess_width in zip(
widths, ratio_divide(excess_width, flex_widths),
)
]
widths = ratio_reduce(
excess_width,
[0 if column.no_wrap else 1 for column in columns],
[width_range.span for width_range in width_ranges],
widths,
)
table_width = sum(widths)
# Reduce rows that are no_wrap
if table_width > max_width:
excess_width = table_width - max_width
widths = ratio_reduce(
excess_width,
[1 if column.no_wrap else 0 for column in columns],
[width_range.span for width_range in width_ranges],
widths,
)
table_width = sum(widths)
if table_width > max_width:
excess_width = table_width - max_width
widths = ratio_reduce(
excess_width,
[width_range.minimum for width_range in width_ranges],
widths,
widths,
)
table_width = sum(widths)
elif table_width < max_width and self.expand:
pad_widths = ratio_divide(max_width - table_width, widths)
pad_widths = ratio_distribute(max_width - table_width, widths)
widths = [_width + pad for _width, pad in zip(widths, pad_widths)]
return widths
def _get_cells(self, column_index: int, column: Column) -> Iterable[_Cell]:
@ -473,11 +494,11 @@ class Table(JupyterMixin):
return pad_left + pad_right
def _measure_column(
self, console: "Console", column_index: int, column: Column, max_width: int
self, console: "Console", column: Column, max_width: int
) -> Measurement:
"""Get the minimum and maximum width of the column."""
padding_width = self._get_padding_width(column_index)
padding_width = self._get_padding_width(column.index)
if column.width is not None:
# Fixed width column
@ -490,18 +511,15 @@ class Table(JupyterMixin):
append_min = min_widths.append
append_max = max_widths.append
get_render_width = Measurement.get
for cell in self._get_cells(column_index, column):
for cell in self._get_cells(column.index, column):
_min, _max = get_render_width(console, cell.renderable, max_width)
append_min(_min)
append_max(_max)
if column.no_wrap:
_width = max(max_widths) if max_widths else max_width
return Measurement(_width, _width)
else:
return Measurement(
max(min_widths) if min_widths else 1,
max(max_widths) if max_widths else max_width,
)
return Measurement(
max(min_widths) if min_widths else 1,
max(max_widths) if max_widths else max_width,
)
def _render(
self, console: "Console", options: "ConsoleOptions", widths: List[int]
@ -517,7 +535,9 @@ class Table(JupyterMixin):
)
)
)
_box = box.SQUARE if (console.legacy_windows and self.box) else self.box
_box: Optional[box.Box] = (
box.SQUARE if (console.legacy_windows and self.box) else self.box
)
new_line = Segment.line()
columns = self.columns
@ -553,6 +573,7 @@ class Table(JupyterMixin):
get_row_style = self.get_row_style
get_style = console.get_style
for index, (first, last, row) in enumerate(loop_first_last(rows)):
header_row = first and show_header
footer_row = last and show_footer
@ -565,7 +586,12 @@ class Table(JupyterMixin):
get_row_style(index - 1 if show_header else index)
)
for width, cell, column in zip(widths, row, columns):
render_options = options.update(width=width, justify=column.justify)
render_options = options.update(
width=width,
justify=column.justify,
no_wrap=column.no_wrap,
overflow=column.overflow,
)
cell_style = table_style + row_style + get_style(cell.style)
lines = console.render_lines(
cell.renderable, render_options, style=cell_style

View file

@ -18,11 +18,12 @@ from typing_extensions import Literal
from ._emoji_replace import _emoji_replace
from ._loop import loop_first_last, loop_last
from ._wrap import divide_line
from .cells import cell_len
from .cells import cell_len, set_cell_size
from .containers import Lines
from .control import strip_control_codes
from .jupyter import JupyterMixin
from .measure import Measurement
from ._pick import pick_bool
from .segment import Segment
from .style import Style, StyleType
@ -30,11 +31,18 @@ if TYPE_CHECKING: # pragma: no cover
from .console import (
Console,
ConsoleOptions,
JustifyValues,
JustifyMethod,
OverflowMethod,
RenderResult,
RenderableType,
)
DEFAULT_JUSTIFY: "JustifyMethod" = "default"
DEFAULT_OVERFLOW: "OverflowMethod" = "fold"
_re_whitespace = re.compile(r"\s+$")
class Span(NamedTuple):
"""A marked up region in some text."""
@ -95,7 +103,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.
"""
@ -104,7 +113,10 @@ class Text(JupyterMixin):
self,
text: str = "",
style: Union[str, Style] = "",
justify: "JustifyValues" = None,
*,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
no_wrap: bool = None,
end: str = "\n",
tab_size: Optional[int] = 8,
spans: List[Span] = None,
@ -113,6 +125,8 @@ class Text(JupyterMixin):
self._text: List[str] = [text] if text else []
self.style = style
self.justify = justify
self.overflow = overflow
self.no_wrap = no_wrap
self.end = end
self.tab_size = tab_size
self._spans: List[Span] = spans if spans is not None else []
@ -153,16 +167,19 @@ class Text(JupyterMixin):
def from_markup(
cls,
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.
@ -171,6 +188,7 @@ class Text(JupyterMixin):
rendered_text = render(text, style, emoji=emoji)
rendered_text.justify = justify
rendered_text.overflow = overflow
return rendered_text
@classmethod
@ -178,7 +196,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":
@ -187,14 +206,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)):
@ -214,11 +236,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]:
@ -235,6 +258,8 @@ class Text(JupyterMixin):
copy_self = Text(
style=self.style,
justify=self.justify,
overflow=self.overflow,
no_wrap=self.no_wrap,
end=self.end,
tab_size=self.tab_size,
)
@ -244,8 +269,10 @@ 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,
end=self.end,
tab_size=self.tab_size,
)
@ -299,7 +326,11 @@ class Text(JupyterMixin):
return style
def highlight_regex(
self, re_highlight: str, style: Union[str, Style] = None, style_prefix: str = ""
self,
re_highlight: str,
style: Union[str, Style] = None,
*,
style_prefix: str = "",
) -> int:
"""Highlight text with a regular expression, where group names are
translated to styles.
@ -331,6 +362,7 @@ class Text(JupyterMixin):
self,
words: Iterable[str],
style: Union[str, Style],
*,
case_sensitive: bool = True,
) -> int:
"""Highlight words with a style.
@ -358,6 +390,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)
@ -378,11 +424,20 @@ class Text(JupyterMixin):
self, console: "Console", options: "ConsoleOptions"
) -> Iterable[Segment]:
tab_size: int = console.tab_size or self.tab_size or 8 # type: ignore
justify = cast(
"JustifyMethod", self.justify or options.justify or DEFAULT_OVERFLOW
)
overflow = cast(
"OverflowMethod", self.overflow or options.overflow or DEFAULT_OVERFLOW
)
lines = self.wrap(
console,
options.max_width,
justify=self.justify or options.justify,
justify=justify,
overflow=overflow,
tab_size=tab_size or 8,
no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),
)
all_lines = Text("\n").join(lines)
yield from all_lines.render(console, end=self.end)
@ -489,9 +544,7 @@ class Text(JupyterMixin):
if tab_size is None:
tab_size = self.tab_size
assert tab_size is not None
result = Text(
style=self.style, justify=self.justify, end=self.end, tab_size=self.tab_size
)
result = self.blank_copy()
append = result.append
for part in parts:
@ -507,18 +560,48 @@ class Text(JupyterMixin):
append(part)
return result
def truncate(
self,
max_width: int,
*,
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.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". 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:
if _overflow == "ellipsis":
self.plain = set_cell_size(self.plain, max_width - 1).rstrip() + ""
else:
self.plain = set_cell_size(self.plain, max_width)
if pad and length < max_width:
spaces = max_width - length
self.plain = f"{self.plain}{' ' * spaces}"
def _trim_spans(self) -> None:
"""Remove or modify any spans that are over the end of the text."""
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:
@ -546,19 +629,22 @@ class Text(JupyterMixin):
def append(
self, text: Union["Text", str], style: Union[str, "Style"] = None
) -> None:
) -> "Text":
"""Add text with an optional style.
Args:
text (Union[Text, str]): A str or Text to append.
style (str, optional): A style name. Defaults to None.
Returns:
text (Text): Returns self for chaining.
"""
if not isinstance(text, (str, Text)):
raise TypeError("Only str or Text can be appended to Text")
if not len(text):
return
return self
if isinstance(text, str):
text = strip_control_codes(text)
self._text.append(text)
@ -583,6 +669,7 @@ class Text(JupyterMixin):
for start, end, style in text._spans
)
self._length += len(text)
return self
def copy_styles(self, text: "Text") -> None:
"""Copy styles from another Text instance.
@ -592,7 +679,7 @@ class Text(JupyterMixin):
"""
self._spans.extend(text._spans)
def split(self, separator="\n", include_separator: bool = False) -> Lines:
def split(self, separator="\n", *, include_separator: bool = False) -> Lines:
r"""Split rich text in to lines, preserving styles.
Args:
@ -646,7 +733,12 @@ class Text(JupyterMixin):
average_line_length = -(-text_length // len(line_ranges))
new_lines = Lines(
Text(text[start:end], style=self.style, justify=self.justify)
Text(
text[start:end],
style=self.style,
justify=self.justify,
overflow=self.overflow,
)
for start, end in line_ranges
)
line_ranges = [
@ -677,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
@ -690,28 +782,50 @@ class Text(JupyterMixin):
self,
console: "Console",
width: int,
justify: "JustifyValues" = "left",
*,
justify: "JustifyMethod" = None,
overflow: "OverflowMethod" = None,
tab_size: int = 8,
no_wrap: bool = None,
) -> 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: "default", "left", "center", "full", "right". Defaults to "default".
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
tab_size (int, optional): Default tab size. Defaults to 8.
no_wrap (bool, optional): Disable wrapping, Defaults to False.
Returns:
Lines: Number of lines.
"""
wrap_justify = cast("JustifyMethod", justify or self.justify or DEFAULT_JUSTIFY)
wrap_overflow = cast(
"OverflowMethod", overflow or self.overflow or DEFAULT_OVERFLOW
)
no_wrap = pick_bool(no_wrap, self.no_wrap, False)
lines: Lines = Lines()
for line in self.split():
if "\t" in line:
line = line.tabs_to_spaces(tab_size)
offsets = divide_line(str(line), width)
new_lines = line.divide(offsets)
if justify:
new_lines.justify(console, width, align=justify)
if no_wrap:
new_lines = Lines([line])
else:
offsets = divide_line(str(line), width, fold=wrap_overflow == "fold")
new_lines = line.divide(offsets)
for line in new_lines:
line.rstrip_end(width)
if wrap_justify:
new_lines.justify(
console, width, justify=wrap_justify, overflow=wrap_overflow
)
for line in new_lines:
line.truncate(width, overflow=wrap_overflow)
lines.extend(new_lines)
return lines
def fit(self, width: int) -> Lines:
@ -729,3 +843,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)))

View file

@ -187,8 +187,8 @@ class Traceback:
if isinstance(exc_value, SyntaxError):
stack.syntax_error = _SyntaxError(
offset=exc_value.offset or 0,
filename=exc_value.filename,
lineno=exc_value.lineno,
filename=exc_value.filename or "?",
lineno=exc_value.lineno or 0,
line=exc_value.text or "",
msg=exc_value.msg,
)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -244,3 +244,9 @@ def test_save_html():
console.save_html(export_path)
with open(export_path, "rt") as html_file:
assert html_file.read() == expected
def test_no_wrap():
console = Console(width=10, file=io.StringIO())
console.print("foo bar baz egg", no_wrap=True)
assert console.file.getvalue() == "foo bar ba\n"

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

11
tests/test_pick.py Normal file
View file

@ -0,0 +1,11 @@
from rich._pick import pick_bool
def test_pick_bool():
assert pick_bool(False, True) == False
assert pick_bool(None, True) == True
assert pick_bool(True, None) == True
assert pick_bool(False, None) == False
assert pick_bool(None, None) == False
assert pick_bool(None, None, False, True) == False
assert pick_bool(None, None, True, False) == True

View file

@ -1,3 +1,5 @@
# encoding=utf-8
import io
from time import time
@ -98,7 +100,6 @@ def make_progress() -> Progress:
def render_progress() -> str:
progress = make_progress()
progress_render = progress.console.file.getvalue()
print(repr(progress_render))
return progress_render
@ -114,13 +115,17 @@ def test_expand_bar() -> None:
)
progress.add_task("foo")
progress.refresh()
expected = "\x1b[38;5;237m━━━━━━━━━\x1b[0m \r\x1b[2K\x1b[38;5;237m━━━━━━━━━\x1b[0m "
assert console.file.getvalue() == expected
expected = "\x1b[38;5;237m━━━━━━━━━━\x1b[0m\r\x1b[2K\x1b[38;5;237m━━━━━━━━━━\x1b[0m"
render_result = console.file.getvalue()
print(repr(render_result))
assert render_result == expected
def test_render() -> None:
expected = "foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \nbar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[1A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[1A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m \negg \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[2A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m \nfoo2 \x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m \x1b[35m 50%\x1b[0m \x1b[36m-:--:--\x1b[0m \r\x1b[2A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m \nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m \nfoo2 \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m "
assert render_progress() == expected
expected = "foo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[1A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[1A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\negg \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\nfoo2 \x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;249;38;114m━\x1b[0m\x1b[38;2;244;38;112m━\x1b[0m\x1b[38;2;230;39;108m━\x1b[0m\x1b[38;2;209;42;102m━\x1b[0m\x1b[38;2;183;44;94m━\x1b[0m\x1b[38;2;153;48;86m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;58;58;58m━\x1b[0m\x1b[38;2;62;57;59m━\x1b[0m\x1b[38;2;76;56;63m━\x1b[0m\x1b[38;2;97;53;69m━\x1b[0m\x1b[38;2;123;51;77m━\x1b[0m \x1b[35m 50%\x1b[0m \x1b[36m-:--:--\x1b[0m\r\x1b[2A\x1b[2Kfoo \x1b[38;5;237m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 0%\x1b[0m \x1b[36m-:--:--\x1b[0m\nbar \x1b[38;2;249;38;114m━━━━━━━━━━━━━━━━━━━━━\x1b[0m\x1b[38;5;237m╺\x1b[0m\x1b[38;5;237m━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m 53%\x1b[0m \x1b[36m-:--:--\x1b[0m\nfoo2 \x1b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m \x1b[35m100%\x1b[0m \x1b[36m0:00:00\x1b[0m"
render_result = render_progress()
print(repr(render_result))
assert render_result == expected
def test_track() -> None:
@ -135,7 +140,7 @@ def test_track() -> None:
_time += 1
console = Console(
file=io.StringIO(), force_terminal=True, width=40, color_system="truecolor"
file=io.StringIO(), force_terminal=True, width=60, color_system="truecolor"
)
test = ["foo", "bar", "baz"]
expected_values = iter(test)
@ -145,7 +150,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 = "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;2;249;38;114m╸\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"
assert result == expected
with pytest.raises(ValueError):
@ -192,7 +197,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:195\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

34
tests/test_table.py Normal file
View file

@ -0,0 +1,34 @@
# encoding=utf-8
import io
from rich.console import Console
from rich.table import Table
def render_tables():
console = Console(width=60, file=io.StringIO())
table = Table(title="test table", caption="table footer", expand=True)
table.add_column("foo", no_wrap=True, overflow="ellipsis")
table.add_column("bar", justify="center")
table.add_column("baz", justify="right")
table.add_row("Averlongwordgoeshere", "banana pancakes", None)
for width in range(10, 60, 5):
console.print(table, width=width)
return console.file.getvalue()
def test_render_table():
expected = "test table\n┏━━┳━━┳━━┓\n┃ ┃ ┃ ┃\n┡━━╇━━╇━━┩\n│ │ │ │\n└──┴──┴──┘\n table \n footer \n test table \n┏━━━━━┳━━━━┳━━┓\n┃ foo ┃ b… ┃ ┃\n┡━━━━━╇━━━━╇━━┩\n│ Av… │ b… │ │\n└─────┴────┴──┘\n table footer \n test table \n┏━━━━━━━━┳━━━━━┳━━━┓\n┃ foo ┃ bar ┃ … ┃\n┡━━━━━━━━╇━━━━━╇━━━┩\n│ Averl… │ ba… │ │\n└────────┴─────┴───┘\n table footer \n test table \n┏━━━━━━━━━━━━┳━━━━━━┳━━━┓\n┃ foo ┃ bar ┃ … ┃\n┡━━━━━━━━━━━━╇━━━━━━╇━━━┩\n│ Averlongw… │ ban… │ │\n└────────────┴──────┴───┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━┩\n│ Averlongword… │ bana… │ │\n└───────────────┴───────┴────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━┓\n┃ foo ┃ bar ┃ b… ┃\n┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━┩\n│ Averlongwordgoe… │ banana… │ │\n└──────────────────┴─────────┴────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshe… │ banana │ │\n│ │ pancakes │ │\n└─────────────────────┴──────────┴─────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana │ │\n│ │ pancakes │ │\n└──────────────────────┴──────────────┴─────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└───────────────────────┴──────────────────┴─────┘\n table footer \n test table \n┏━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━┓\n┃ foo ┃ bar ┃ baz ┃\n┡━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━┩\n│ Averlongwordgoeshere │ banana pancakes │ │\n└──────────────────────────┴────────────────────┴─────┘\n table footer \n"
assert render_tables() == expected
if __name__ == "__main__":
render = render_tables()
print(render)
print(repr(render))

View file

@ -173,7 +173,7 @@ def test_highlight_words():
assert test._spans == [Span(0, 2, "red")]
test = Text("AB Ab aB ab")
count = test.highlight_words(words, "red", False)
count = test.highlight_words(words, "red", case_sensitive=False)
assert count == 4
@ -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 "
@ -453,3 +454,30 @@ def test_get_style_at_offset():
text = Text.from_markup("Hello [b]World[/b]")
assert text.get_style_at_offset(console, 0) == Style()
assert text.get_style_at_offset(console, 6) == Style(bold=True)
@pytest.mark.parametrize(
"input, count, expected",
[
("Hello", 10, "Hello"),
("Hello", 5, "Hello"),
("Hello", 4, "Hel…"),
("Hello", 3, "He…"),
("Hello", 2, "H…"),
("Hello", 1, ""),
],
)
def test_truncate_ellipsis(input, count, expected):
text = Text(input)
text.truncate(count, overflow="ellipsis")
assert text.plain == expected
@pytest.mark.parametrize(
"input, count, expected",
[("Hello", 5, "Hello"), ("Hello", 10, "Hello "), ("Hello", 3, "He…"),],
)
def test_truncate_ellipsis_pad(input, count, expected):
text = Text(input)
text.truncate(count, overflow="ellipsis", pad=True)
assert text.plain == expected

View file

@ -1,5 +1,5 @@
from rich._loop import loop_first, loop_last, loop_first_last
from rich._ratio import ratio_divide
from rich._ratio import ratio_distribute
def test_loop_first():
@ -29,10 +29,10 @@ def test_loop_first_last():
assert next(iterable) == (False, True, "lemons")
def test_ratio_divide():
assert ratio_divide(10, [1]) == [10]
assert ratio_divide(10, [1, 1]) == [5, 5]
assert ratio_divide(12, [1, 3]) == [3, 9]
assert ratio_divide(0, [1, 3]) == [0, 0]
assert ratio_divide(0, [1, 3], [1, 1]) == [1, 1]
assert ratio_divide(10, [1, 0]) == [10, 0]
def test_ratio_distribute():
assert ratio_distribute(10, [1]) == [10]
assert ratio_distribute(10, [1, 1]) == [5, 5]
assert ratio_distribute(12, [1, 3]) == [3, 9]
assert ratio_distribute(0, [1, 3]) == [0, 0]
assert ratio_distribute(0, [1, 3], [1, 1]) == [1, 1]
assert ratio_distribute(10, [1, 0]) == [10, 0]