Merge branch 'master' into highlight_regex_compiled

This commit is contained in:
Will McGugan 2024-09-30 15:39:33 +01:00 committed by GitHub
commit b5d063ca16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 359 additions and 82 deletions

View File

@ -6,13 +6,12 @@ jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
include:
- { os: ubuntu-latest, python-version: "3.7" }
- { os: windows-latest, python-version: "3.7" }
- { os: macos-12, python-version: "3.7" }
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
exclude:
- { os: windows-latest, python-version: "3.13" }
defaults:
run:
shell: bash
@ -22,6 +21,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install and configure Poetry
# TODO: workaround for https://github.com/snok/install-poetry/issues/94
uses: snok/install-poetry@v1.3.4

View File

@ -5,9 +5,32 @@ 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).
## Unreleased
### Changed
- Rich will display tracebacks with finely grained error locations on python 3.11+ https://github.com/Textualize/rich/pull/3486
### Fixed
- Fixed issue with Segment._split_cells https://github.com/Textualize/rich/pull/3506
- Fix auto detection of terminal size on Windows https://github.com/Textualize/rich/pull/2916
### Added
- Add a new `column` object `IterationSpeedColumn`. https://github.com/Textualize/rich/pull/3332
## [13.8.1] - 2024-09-10
### Fixed
- Added support for Python 3.13 https://github.com/Textualize/rich/pull/3481
- Fixed infinite loop when appending Text to same instance https://github.com/Textualize/rich/pull/3480
## [13.8.0] - 2024-08-26
### Fixed
- Fixed `Table` rendering of box elements so "footer" elements truly appear at bottom of table, "mid" elements in main table body.
@ -17,8 +40,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Progress track thread is now a daemon thread https://github.com/Textualize/rich/pull/3402
- Fixed cached hash preservation upon clearing meta and links https://github.com/Textualize/rich/issues/2942
- Fixed overriding the `background_color` of `Syntax` not including padding https://github.com/Textualize/rich/issues/3295
- Fixed pretty printing of dataclasses with a default repr in Python 3.13 https://github.com/Textualize/rich/pull/3455
- Fixed selective enabling of highlighting when disabled in the `Console` https://github.com/Textualize/rich/issues/3419
- Fixed BrokenPipeError writing an error message https://github.com/Textualize/rich/pull/3468
- Fixed superfluous space above Markdown tables https://github.com/Textualize/rich/pull/3469
- Fixed issue with record and capture interaction https://github.com/Textualize/rich/pull/3470
- Fixed control codes breaking in `append_tokens` https://github.com/Textualize/rich/pull/3471
- Fixed exception pretty printing a dataclass with missing fields https://github.com/Textualize/rich/pull/3472
### Changed
@ -42,6 +70,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated the widths of some characters https://github.com/Textualize/rich/pull/3289
### Added
- Included a `name` attribute to the `Spinner` class https://github.com/Textualize/rich/pull/3359
## [13.7.0] - 2023-11-15
### Added
@ -2056,6 +2088,8 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr
- First official release, API still to be stabilized
[13.8.1]: https://github.com/textualize/rich/compare/v13.8.0...v13.8.1
[13.8.0]: https://github.com/textualize/rich/compare/v13.7.1...v13.8.0
[13.7.1]: https://github.com/textualize/rich/compare/v13.7.0...v13.7.1
[13.7.0]: https://github.com/textualize/rich/compare/v13.6.0...v13.7.0
[13.6.0]: https://github.com/textualize/rich/compare/v13.5.3...v13.6.0

View File

@ -20,6 +20,7 @@ The following people have contributed to the development of Rich:
- [Aryaz Eghbali](https://github.com/AryazE)
- [Oleksis Fraga](https://github.com/oleksis)
- [Andy Gimblett](https://github.com/gimbo)
- [Kai Giokas](https://github.com/kaisforza)
- [Tom Gooding](https://github.com/TomJGooding)
- [Michał Górny](https://github.com/mgorny)
- [Nok Lam Chan](https://github.com/noklam)
@ -63,6 +64,7 @@ The following people have contributed to the development of Rich:
- [Tushar Sadhwani](https://github.com/tusharsadhwani)
- [Luca Salvarani](https://github.com/LukeSavefrogs)
- [Paul Sanders](https://github.com/sanders41)
- [Louis Sautier](https://github.com/sbraz)
- [Tim Savage](https://github.com/timsavage)
- [Anthony Shaw](https://github.com/tonybaloney)
- [Nicolas Simonds](https://github.com/0xDEC0DE)
@ -86,3 +88,6 @@ The following people have contributed to the development of Rich:
- [Bernhard Wagner](https://github.com/bwagner)
- [Aaron Beaudoin](https://github.com/AaronBeaudoin)
- [Sam Woodward](https://github.com/PyWoody)
- [L. Yeung](https://github.com/lewis-yeung)
- [chthollyphile](https://github.com/chthollyphile)
- [Jonathan Helmus](https://github.com/jjhelmus)

View File

@ -1,4 +1,4 @@
alabaster==0.7.12
alabaster==1.0.0
Sphinx==7.3.7
sphinx-rtd-theme==2.0.0
sphinx-copybutton==0.5.1

View File

@ -131,7 +131,7 @@ Columns
You may customize the columns in the progress display with the positional arguments to the :class:`~rich.progress.Progress` constructor. The columns are specified as either a `format string <https://docs.python.org/3/library/string.html#formatspec>`_ or a :class:`~rich.progress.ProgressColumn` object.
Format strings will be rendered with a single value `"task"` which will be a :class:`~rich.progress.Task` instance. For example ``"{task.description}"`` would display the task description in the column, and ``"{task.completed} of {task.total}"`` would display how many of the total steps have been completed. Additional fields passed via keyword arguments to `~rich.progress.Progress.update` are store in ``task.fields``. You can add them to a format string with the following syntax: ``"extra info: {task.fields[extra]}"``.
Format strings will be rendered with a single value `"task"` which will be a :class:`~rich.progress.Task` instance. For example ``"{task.description}"`` would display the task description in the column, and ``"{task.completed} of {task.total}"`` would display how many of the total steps have been completed. Additional fields passed via keyword arguments to `~rich.progress.Progress.update` are stored in ``task.fields``. You can add them to a format string with the following syntax: ``"extra info: {task.fields[extra]}"``.
The default columns are equivalent to the following::
@ -163,6 +163,7 @@ The following column objects are available:
- :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes).
- :class:`~rich.progress.SpinnerColumn` Displays a "spinner" animation.
- :class:`~rich.progress.RenderableColumn` Displays an arbitrary Rich renderable in the column.
- :class:`~rich.progress.IterationSpeedColumn` Displays iteration speed in it/s (iterations per second).
To implement your own columns, extend the :class:`~rich.progress.ProgressColumn` class and use it as you would the other columns.

View File

@ -73,7 +73,8 @@ def download(urls: Iterable[str], dest_dir: str):
if __name__ == "__main__":
# Try with https://releases.ubuntu.com/20.04/ubuntu-20.04.3-desktop-amd64.iso
# Try with https://releases.ubuntu.com/noble/ubuntu-24.04-desktop-amd64.iso
# and https://releases.ubuntu.com/noble/ubuntu-24.04-live-server-amd64.iso
if sys.argv[1:]:
download(sys.argv[1:], "./")
else:

6
poetry.lock generated
View File

@ -884,13 +884,13 @@ files = [
[[package]]
name = "virtualenv"
version = "20.26.3"
version = "20.26.4"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
{file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"},
{file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"},
]
[package.dependencies]

View File

@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/Textualize/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "13.7.1"
version = "13.8.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
@ -21,6 +21,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
include = ["rich/py.typed"]

View File

@ -1005,19 +1005,13 @@ class Console:
width: Optional[int] = None
height: Optional[int] = None
if WINDOWS: # pragma: no cover
for file_descriptor in _STD_STREAMS_OUTPUT if WINDOWS else _STD_STREAMS:
try:
width, height = os.get_terminal_size()
width, height = os.get_terminal_size(file_descriptor)
except (AttributeError, ValueError, OSError): # Probably not a terminal
pass
else:
for file_descriptor in _STD_STREAMS:
try:
width, height = os.get_terminal_size(file_descriptor)
except (AttributeError, ValueError, OSError):
pass
else:
break
else:
break
columns = self._environ.get("COLUMNS")
if columns is not None and columns.isdigit():
@ -2029,7 +2023,7 @@ class Console:
"""Write the buffer to the output file."""
with self._lock:
if self.record:
if self.record and not self._buffer_index:
with self._record_buffer_lock:
self._record_buffer.extend(self._buffer[:])

View File

@ -120,6 +120,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
"traceback.exc_type": Style(color="bright_red", bold=True),
"traceback.exc_value": Style.null(),
"traceback.offset": Style(color="bright_red", bold=True),
"traceback.error_range": Style(underline=True, bold=True, dim=False),
"bar.back": Style(color="grey23"),
"bar.complete": Style(color="rgb(249,38,114)"),
"bar.finished": Style(color="rgb(114,156,31)"),

View File

@ -98,7 +98,7 @@ class ReprHighlighter(RegexHighlighter):
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)",
r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?",
r"(?<![\\\w])(?P<str>b?'''.*?(?<!\\)'''|b?'.*?(?<!\\)'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)",
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)",
),
]

View File

@ -37,7 +37,7 @@ class Live(JupyterMixin, RenderHook):
Args:
renderable (RenderableType, optional): The renderable to live display. Defaults to displaying nothing.
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
console (Console, optional): Optional Console instance. Defaults to an internal Console instance writing to stdout.
screen (bool, optional): Enable alternate screen mode. Defaults to False.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 4.

View File

@ -677,7 +677,7 @@ class Markdown(JupyterMixin):
and context.stack.top.on_child_close(context, element)
)
if should_render:
if new_line:
if new_line and node_type != "inline":
yield _new_line_segment
yield from console.render(element, context.options)

View File

@ -3,6 +3,7 @@ import collections
import dataclasses
import inspect
import os
import reprlib
import sys
from array import array
from collections import Counter, UserDict, UserList, defaultdict, deque
@ -78,7 +79,10 @@ def _is_dataclass_repr(obj: object) -> bool:
# Digging in to a lot of internals here
# Catching all exceptions in case something is missing on a non CPython implementation
try:
return obj.__repr__.__code__.co_filename == dataclasses.__file__
return obj.__repr__.__code__.co_filename in (
dataclasses.__file__,
reprlib.__file__,
)
except Exception: # pragma: no coverage
return False
@ -777,7 +781,9 @@ def traverse(
)
for last, field in loop_last(
field for field in fields(obj) if field.repr
field
for field in fields(obj)
if field.repr and hasattr(obj, field.name)
):
child_node = _traverse(getattr(obj, field.name), depth=depth + 1)
child_node.key_repr = field.name

View File

@ -39,6 +39,11 @@ if sys.version_info >= (3, 8):
else:
from typing_extensions import Literal # pragma: no cover
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self # pragma: no cover
from . import filesize, get_console
from .console import Console, Group, JustifyMethod, RenderableType
from .highlighter import Highlighter
@ -917,6 +922,25 @@ class TransferSpeedColumn(ProgressColumn):
return Text(f"{data_speed}/s", style="progress.data.speed")
class IterationSpeedColumn(ProgressColumn):
"""Renders iterations per second, e.g. '11.4 it/s'."""
def render(self, task: "Task") -> Text:
last_speed = task.last_speed if hasattr(task, 'last_speed') else None
if task.finished and last_speed is not None:
return Text(f"{last_speed} it/s", style="progress.data.speed")
if task.speed is None:
return Text("", style="progress.data.speed")
unit, suffix = filesize.pick_unit_and_suffix(
int(task.speed),
["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
1000,
)
data_speed = task.speed / unit
task.last_speed = f"{data_speed:.1f}{suffix}"
return Text(f"{task.last_speed} it/s", style="progress.data.speed")
class ProgressSample(NamedTuple):
"""Sample of progress for a given time."""
@ -1056,7 +1080,7 @@ class Progress(JupyterMixin):
"""Renders an auto-updating progress bar(s).
Args:
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
console (Console, optional): Optional Console instance. Defaults to an internal Console instance writing to stdout.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`.
refresh_per_second (Optional[float], optional): Number of times per second to refresh the progress information or None to use default (10). Defaults to None.
speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30.
@ -1170,7 +1194,7 @@ class Progress(JupyterMixin):
if not self.console.is_interactive and not self.console.is_jupyter:
self.console.print()
def __enter__(self) -> "Progress":
def __enter__(self) -> Self:
self.start()
return self

View File

@ -109,16 +109,29 @@ class Segment(NamedTuple):
@classmethod
@lru_cache(1024 * 16)
def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]:
"""Split a segment in to two at a given cell position.
Note that splitting a double-width character, may result in that character turning
into two spaces.
Args:
segment (Segment): A segment to split.
cut (int): A cell position to cut on.
Returns:
A tuple of two segments.
"""
text, style, control = segment
_Segment = Segment
cell_length = segment.cell_length
if cut >= cell_length:
return segment, _Segment("", style, control)
cell_size = get_character_cell_size
pos = int((cut / cell_length) * (len(text) - 1))
pos = int((cut / cell_length) * (len(text))) - 1
if pos < 0:
pos = 0
before = text[:pos]
cell_pos = cell_len(before)

View File

@ -38,6 +38,7 @@ class Spinner:
self.text: "Union[RenderableType, Text]" = (
Text.from_markup(text) if isinstance(text, str) else text
)
self.name = name
self.frames = cast(List[str], spinner["frames"])[:]
self.interval = cast(float, spinner["interval"])
self.start_time: Optional[float] = None

View File

@ -221,6 +221,7 @@ class _SyntaxHighlightRange(NamedTuple):
style: StyleType
start: SyntaxPosition
end: SyntaxPosition
style_before: bool = False
class Syntax(JupyterMixin):
@ -534,7 +535,11 @@ class Syntax(JupyterMixin):
return text
def stylize_range(
self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
self,
style: StyleType,
start: SyntaxPosition,
end: SyntaxPosition,
style_before: bool = False,
) -> None:
"""
Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
@ -544,8 +549,11 @@ class Syntax(JupyterMixin):
style (StyleType): The style to apply.
start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
style_before (bool): Apply the style before any existing styles.
"""
self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
self._stylized_ranges.append(
_SyntaxHighlightRange(style, start, end, style_before)
)
def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
background_style = self._theme.get_background_style() + self.background_style
@ -785,7 +793,10 @@ class Syntax(JupyterMixin):
newlines_offsets, stylized_range.end
)
if start is not None and end is not None:
text.stylize(stylized_range.style, start, end)
if stylized_range.style_before:
text.stylize_before(stylized_range.style, start, end)
else:
text.stylize(stylized_range.style, start, end)
def _process_code(self, code: str) -> Tuple[bool, str]:
"""

View File

@ -1000,7 +1000,7 @@ class Text(JupyterMixin):
self._text.append(text.plain)
self._spans.extend(
_Span(start + text_length, end + text_length, style)
for start, end, style in text._spans
for start, end, style in text._spans.copy()
)
self._length += len(text)
return self
@ -1022,7 +1022,7 @@ class Text(JupyterMixin):
self._text.append(text.plain)
self._spans.extend(
_Span(start + text_length, end + text_length, style)
for start, end, style in text._spans
for start, end, style in text._spans.copy()
)
self._length += len(text)
return self
@ -1043,6 +1043,7 @@ class Text(JupyterMixin):
_Span = Span
offset = len(self)
for content, style in tokens:
content = strip_control_codes(content)
append_text(content)
if style:
append_span(_Span(offset, offset + len(content), style))

View File

@ -1,7 +1,9 @@
import inspect
import linecache
import os
import sys
from dataclasses import dataclass, field
from itertools import islice
from traceback import walk_tb
from types import ModuleType, TracebackType
from typing import (
@ -179,6 +181,7 @@ class Frame:
name: str
line: str = ""
locals: Optional[Dict[str, pretty.Node]] = None
last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None
@dataclass
@ -442,6 +445,35 @@ class Traceback:
for frame_summary, line_no in walk_tb(traceback):
filename = frame_summary.f_code.co_filename
last_instruction: Optional[Tuple[Tuple[int, int], Tuple[int, int]]]
last_instruction = None
if sys.version_info >= (3, 11):
instruction_index = frame_summary.f_lasti // 2
instruction_position = next(
islice(
frame_summary.f_code.co_positions(),
instruction_index,
instruction_index + 1,
)
)
(
start_line,
end_line,
start_column,
end_column,
) = instruction_position
if (
start_line is not None
and end_line is not None
and start_column is not None
and end_column is not None
):
last_instruction = (
(start_line, start_column),
(end_line, end_column),
)
if filename and not filename.startswith("<"):
if not os.path.isabs(filename):
filename = os.path.join(_IMPORT_CWD, filename)
@ -452,16 +484,20 @@ class Traceback:
filename=filename or "?",
lineno=line_no,
name=frame_summary.f_code.co_name,
locals={
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
}
if show_locals
else None,
locals=(
{
key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in get_locals(frame_summary.f_locals.items())
if not (inspect.isfunction(value) or inspect.isclass(value))
}
if show_locals
else None
),
last_instruction=last_instruction,
)
append(frame)
if frame_summary.f_locals.get("_rich_traceback_guard", False):
@ -711,6 +747,14 @@ class Traceback:
(f"\n{error}", "traceback.error"),
)
else:
if frame.last_instruction is not None:
start, end = frame.last_instruction
syntax.stylize_range(
style="traceback.error_range",
start=start,
end=end,
style_before=True,
)
yield (
Columns(
[
@ -725,12 +769,12 @@ class Traceback:
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console()
install(show_locals=True)
import sys
def bar(a: Any) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
def bar(
a: Any,
) -> None: # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
one = 1
print(one / a)
@ -748,12 +792,6 @@ if __name__ == "__main__": # pragma: no cover
bar(a)
def error() -> None:
try:
try:
foo(0)
except:
slfkjsldkfj # type: ignore[name-defined]
except:
console.print_exception(show_locals=True)
foo(0)
error()

View File

@ -381,23 +381,6 @@ def test_capture():
assert capture.get() == "Hello\n"
def test_capture_and_record(capsys):
recorder = Console(record=True)
recorder.print("ABC")
with recorder.capture() as capture:
recorder.print("Hello")
assert capture.get() == "Hello\n"
recorded_text = recorder.export_text()
out, err = capsys.readouterr()
assert recorded_text == "ABC\nHello\n"
assert capture.get() == "Hello\n"
assert out == "ABC\n"
def test_input(monkeypatch, capsys):
def fake_input(prompt=""):
console.file.write(prompt)
@ -1038,3 +1021,24 @@ def test_brokenpipeerror() -> None:
proc2.wait()
assert proc1.returncode == 1
assert proc2.returncode == 0
def test_capture_and_record() -> None:
"""Regression test for https://github.com/Textualize/rich/issues/2563"""
console = Console(record=True)
print("Before Capture started:")
console.print("[blue underline]Print 0")
with console.capture() as capture:
console.print("[blue underline]Print 1")
console.print("[blue underline]Print 2")
console.print("[blue underline]Print 3")
console.print("[blue underline]Print 4")
capture_content = capture.get()
print(repr(capture_content))
assert capture_content == "Print 1\nPrint 2\nPrint 3\nPrint 4\n"
recorded_content = console.export_text()
print(repr(recorded_content))
assert recorded_content == "Print 0\n"

View File

@ -134,6 +134,7 @@ highlight_tests = [
(" http://example.org ", [Span(1, 19, "repr.url")]),
(" http://example.org/index.html ", [Span(1, 30, "repr.url")]),
(" http://example.org/index.html#anchor ", [Span(1, 37, "repr.url")]),
("https://www.youtube.com/@LinusTechTips", [Span(0, 38, "repr.url")]),
(
" http://example.org/index.html?param1=value1 ",
[

File diff suppressed because one or more lines are too long

View File

@ -668,7 +668,10 @@ def test_attrs_broken_310() -> None:
del foo.bar
result = pretty_repr(foo)
print(repr(result))
expected = "Foo(bar=AttributeError(\"'Foo' object has no attribute 'bar'\"))"
if sys.version_info >= (3, 13):
expected = "Foo(\n bar=AttributeError(\"'tests.test_pretty.test_attrs_broken_310.<locals>.Foo' object has no attribute 'bar'\")\n)"
else:
expected = "Foo(bar=AttributeError(\"'Foo' object has no attribute 'bar'\"))"
assert result == expected
@ -734,3 +737,23 @@ def test_tuple_rich_repr_default() -> None:
yield None, (1,), (1,)
assert pretty_repr(Foo()) == "Foo()"
def test_dataclass_no_attribute() -> None:
"""Regression test for https://github.com/Textualize/rich/issues/3417"""
from dataclasses import dataclass, field
@dataclass(eq=False)
class BadDataclass:
item: int = field(init=False)
# item is not provided
bad_data_class = BadDataclass()
console = Console()
with console.capture() as capture:
console.print(bad_data_class)
expected = "BadDataclass()\n"
result = capture.get()
assert result == expected

View File

@ -26,6 +26,7 @@ from rich.progress import (
TimeRemainingColumn,
TotalFileSizeColumn,
TransferSpeedColumn,
IterationSpeedColumn,
_TrackThread,
track,
)
@ -358,6 +359,7 @@ def test_columns() -> None:
TransferSpeedColumn(),
MofNCompleteColumn(),
MofNCompleteColumn(separator=" of "),
IterationSpeedColumn(),
transient=True,
console=console,
auto_refresh=False,
@ -377,7 +379,7 @@ def test_columns() -> None:
result = replace_link_ids(console.file.getvalue())
print(repr(result))
expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kfoo\ntest foo \x1b[38;5;237m━━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m\ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:18\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes \x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m\r\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes/s\x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:34\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of 10\x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s \x1b[0m \ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:00\x1b[0m \x1b[33m0:00:29\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 bytes\x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes/s\x1b[0m \n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
expected = "\x1b[?25ltest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m \n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mhello \ntest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m \n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Kworld\ntest foo \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:07\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m10 bytes\x1b[0m \x1b[32m0/10 \x1b[0m \x1b[31m?\x1b[0m \x1b[32m 0/10\x1b[0m \x1b[32m 0 of 10\x1b[0m \n \x1b[32mbytes \x1b[0m \ntest bar \x1b[38;5;237m━━━━━━━━━\x1b[0m \x1b[36m-:--:--\x1b[0m \x1b[33m0:00:19\x1b[0m \x1b[32m0 bytes\x1b[0m \x1b[32m7 bytes \x1b[0m \x1b[32m0/7 bytes\x1b[0m \x1b[31m?\x1b[0m \x1b[32m0/7 \x1b[0m \x1b[32m0 of 7 \x1b[0m \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of \x1b[0m \x1b[31m1.3 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s\x1b[0m \x1b[32m10 \x1b[0m \x1b[31mit/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7\x1b[0m \x1b[31m1.5 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes…\x1b[0m \x1b[31mit/s \x1b[0m\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2Ktest foo \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m12 \x1b[0m \x1b[32m10 \x1b[0m \x1b[32m12/10 \x1b[0m \x1b[31m1 \x1b[0m \x1b[32m12/10\x1b[0m \x1b[32m12 of \x1b[0m \x1b[31m1.3 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbyte/s\x1b[0m \x1b[32m10 \x1b[0m \x1b[31mit/s \x1b[0m\ntest bar \x1b[38;2;114;156;31m━━━━━━\x1b[0m \x1b[36m0:00:\x1b[0m \x1b[33m0:00:…\x1b[0m \x1b[32m16 \x1b[0m \x1b[32m7 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[31m2 \x1b[0m \x1b[32m16/7 \x1b[0m \x1b[32m16 of 7\x1b[0m \x1b[31m1.5 \x1b[0m\n \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[32mbytes \x1b[0m \x1b[31mbytes…\x1b[0m \x1b[31mit/s \x1b[0m\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
assert result == expected

View File

@ -2,6 +2,7 @@ from io import StringIO
import pytest
from rich.cells import cell_len
from rich.segment import ControlType, Segment, SegmentLines, Segments
from rich.style import Style
@ -284,6 +285,34 @@ def test_split_cells_emoji(text, split, result):
assert Segment(text).split_cells(split) == result
def test_split_cells_mixed() -> None:
"""Check that split cells splits on cell positions."""
# Caused https://github.com/Textualize/textual/issues/4996 in Textual
test = Segment("早乙女リリエル (CV: 徳井青)")
for position in range(1, test.cell_length):
left, right = Segment.split_cells(test, position)
assert cell_len(left.text) == position
assert cell_len(right.text) == test.cell_length - position
def test_split_cells_doubles() -> None:
"""Check that split cells splits on cell positions with all double width characters."""
test = Segment("" * 20)
for position in range(1, test.cell_length):
left, right = Segment.split_cells(test, position)
assert cell_len(left.text) == position
assert cell_len(right.text) == test.cell_length - position
def test_split_cells_single() -> None:
"""Check that split cells splits on cell positions with all single width characters."""
test = Segment("A" * 20)
for position in range(1, test.cell_length):
left, right = Segment.split_cells(test, position)
assert cell_len(left.text) == position
assert cell_len(right.text) == test.cell_length - position
def test_segment_lines_renderable():
lines = [[Segment("hello"), Segment(" "), Segment("world")], [Segment("foo")]]
segment_lines = SegmentLines(lines)

View File

@ -1034,3 +1034,34 @@ def test_extend_style():
text.extend_style(2)
assert text.plain == "foo bar "
assert text.spans == [Span(0, 3, "red"), Span(4, 9, "bold")]
def test_append_tokens() -> None:
"""Regression test for https://github.com/Textualize/rich/issues/3014"""
console = Console()
t = Text().append_tokens(
[
(
"long text that will be wrapped with a control code \r\n",
"red",
),
]
)
with console.capture() as capture:
console.print(t, width=40)
output = capture.get()
print(repr(output))
assert output == "long text that will be wrapped with a \ncontrol code \n\n"
def test_append_loop_regression() -> None:
"""Regression text for https://github.com/Textualize/rich/issues/3479"""
a = Text("one", "blue")
a.append(a)
assert a.plain == "oneone"
b = Text("two", "blue")
b.append_text(b)
assert b.plain == "twotwo"

View File

@ -327,3 +327,34 @@ def test_rich_traceback_omit_optional_local_flag(
assert len(frames) == expected_frames_length
frame_names = [f.name for f in frames]
assert frame_names == expected_frame_names
@pytest.mark.skipif(
sys.version_info.minor >= 11, reason="Not applicable after Python 3.11"
)
def test_traceback_finely_grained_missing() -> None:
"""Before 3.11, the last_instruction should be None"""
try:
1 / 0
except:
traceback = Traceback()
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
assert last_instruction is None
@pytest.mark.skipif(
sys.version_info.minor < 11, reason="Not applicable before Python 3.11"
)
def test_traceback_finely_grained() -> None:
"""Check that last instruction is populated."""
try:
1 / 0
except:
traceback = Traceback()
last_instruction = traceback.trace.stacks[-1].frames[-1].last_instruction
assert last_instruction is not None
assert isinstance(last_instruction, tuple)
assert len(last_instruction) == 2
start, end = last_instruction
print(start, end)
assert start[0] == end[0]

View File

@ -3,7 +3,7 @@ minversion = 4.0.0
envlist =
lint
docs
py{37,38,39,310,311}
py{37,38,39,310,311,312,313}
isolated_build = True
[testenv]