mirror of https://github.com/Textualize/rich.git
Merge branch 'master' into highlight_regex_compiled
This commit is contained in:
commit
b5d063ca16
|
@ -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
|
||||
|
|
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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[:])
|
||||
|
||||
|
|
|
@ -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)"),
|
||||
|
|
|
@ -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$_+!`(),.?/;:&=%#~@]*)",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]:
|
||||
"""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue