Merge branch 'master' of github.com:Textualize/rich into capture-echo

This commit is contained in:
Darren Burns 2022-06-22 11:49:26 +01:00
commit 6e7074b4cb
No known key found for this signature in database
GPG Key ID: B0939B45037DC345
18 changed files with 290 additions and 170 deletions

View File

@ -5,7 +5,15 @@ 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]
## [12.4.5] - Unreleased
### Added
- Environment variables `JUPYTER_COLUMNS` and `JUPYTER_LINES` to control width and height of console in Jupyter
### Changed
- Default width of Jupyter console size is increased to 115
### Fixed
@ -15,6 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix crashes that can happen with `inspect` when docstrings contain some special control codes https://github.com/Textualize/rich/pull/2294
- Fix edges used in first row of tables when `show_header=False` https://github.com/Textualize/rich/pull/2330
- Fix interaction between `Capture` contexts and `Console(record=True)` https://github.com/Textualize/rich/pull/2343
- Fixed hash issue in Styles class https://github.com/Textualize/rich/pull/2346
### Changed
- `Style.__add__` will no longer return `NotImplemented`
- Remove rich.\_lru_cache
### Added

View File

@ -12,6 +12,7 @@ The following people have contributed to the development of Rich:
- [Pete Davison](https://github.com/pd93)
- [James Estevez](https://github.com/jstvz)
- [Oleksis Fraga](https://github.com/oleksis)
- [Andy Gimblett](https://github.com/gimbo)
- [Michał Górny](https://github.com/mgorny)
- [Leron Gray](https://github.com/daddycocoaman)
- [Kenneth Hoste](https://github.com/boegel)
@ -24,6 +25,7 @@ The following people have contributed to the development of Rich:
- [Alexander Mancevice](https://github.com/amancevice)
- [Will McGugan](https://github.com/willmcgugan)
- [Paul McGuire](https://github.com/ptmcg)
- [Antony Milne](https://github.com/AntonyMilneQB)
- [Nathan Page](https://github.com/nathanrpage97)
- [Avi Perl](https://github.com/avi-perl)
- [Laurent Peuch](https://github.com/psycojoker)

View File

@ -1,4 +1,4 @@
alabaster==0.7.12
Sphinx==4.5.0
Sphinx==5.0.2
sphinx-rtd-theme==1.0.0
sphinx-copybutton==0.5.0

View File

@ -429,3 +429,5 @@ Rich respects some standard environment variables.
Setting the environment variable ``TERM`` to ``"dumb"`` or ``"unknown"`` will disable color/style and some features that require moving the cursor, such as progress bars.
If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output.
If ``width``/``height`` arguments are not explicitly provided as arguments to ``Console`` then the environment variables ``COLUMNS``/``LINES`` can be used to set the console width/height. ``JUPYTER_COLUMNS``/``JUPYTER_LINES`` behave similarly and are used in Jupyter.

50
poetry.lock generated
View File

@ -490,7 +490,7 @@ python-versions = "*"
[[package]]
name = "mypy"
version = "0.950"
version = "0.961"
description = "Optional static typing for Python"
category = "dev"
optional = false
@ -1065,7 +1065,7 @@ jupyter = ["ipywidgets"]
[metadata]
lock-version = "1.1"
python-versions = "^3.6.3"
content-hash = "db93dc88a30c445999c422ad140ff15c2d28d0017efe262ce0f750cb42b45baf"
content-hash = "b91f46d585ba68100ec1683af412ef096290262d34257fbce5c7b8353e9dc227"
[metadata.files]
appnope = [
@ -1389,29 +1389,29 @@ mistune = [
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
]
mypy = [
{file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"},
{file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"},
{file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"},
{file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"},
{file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"},
{file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"},
{file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"},
{file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"},
{file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"},
{file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"},
{file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"},
{file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"},
{file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"},
{file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"},
{file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"},
{file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"},
{file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"},
{file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"},
{file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"},
{file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"},
{file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"},
{file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"},
{file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"},
{file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"},
{file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"},
{file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"},
{file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"},
{file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"},
{file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"},
{file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"},
{file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"},
{file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"},
{file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"},
{file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"},
{file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"},
{file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"},
{file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"},
{file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"},
{file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"},
{file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"},
{file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"},
{file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"},
{file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"},
{file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"},
{file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"},
{file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},

View File

@ -40,7 +40,7 @@ jupyter = ["ipywidgets"]
[tool.poetry.dev-dependencies]
pytest = "^7.0.0"
black = "^22.3"
mypy = "^0.950"
mypy = "^0.961"
pytest-cov = "^3.0.0"
attrs = "^21.4.0"
types-dataclasses = "^0.6.4"

View File

@ -1,38 +0,0 @@
from typing import Dict, Generic, TypeVar, TYPE_CHECKING
import sys
CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
if sys.version_info < (3, 9):
from typing_extensions import OrderedDict
else:
from collections import OrderedDict
class LRUCache(OrderedDict[CacheKey, CacheValue]):
"""
A dictionary-like container that stores a given maximum items.
If an additional item is added when the LRUCache is full, the least
recently used key is discarded to make room for the new item.
"""
def __init__(self, cache_size: int) -> None:
self.cache_size = cache_size
super().__init__()
def __setitem__(self, key: CacheKey, value: CacheValue) -> None:
"""Store a new views, potentially discarding an old value."""
if key not in self:
if len(self) >= self.cache_size:
self.popitem(last=False)
super().__setitem__(key, value)
def __getitem__(self, key: CacheKey) -> CacheValue:
"""Gets the item, but also makes it most recent."""
value: CacheValue = super().__getitem__(key)
super().__delitem__(key)
super().__setitem__(key, value)
return value

View File

@ -1,15 +1,15 @@
import re
from functools import lru_cache
from typing import Dict, List
from typing import Callable, List
from ._cell_widths import CELL_WIDTHS
from ._lru_cache import LRUCache
# Regex to match sequence of the most common character ranges
_is_single_cell_widths = re.compile("^[\u0020-\u006f\u00a0\u02ff\u0370-\u0482]*$").match
def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int:
@lru_cache(4096)
def _cached_cell_len(text: str) -> int:
"""Get the number of cells required to display text.
Args:
@ -18,14 +18,24 @@ def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int:
Returns:
int: Get the number of cells required to display text.
"""
cached_result = _cache.get(text, None)
if cached_result is not None:
return cached_result
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
if len(text) <= 512:
_cache[text] = total_size
return total_size
def cell_len(text: str, _cell_len: Callable[[str], int] = _cached_cell_len) -> int:
"""Get the number of cells required to display text.
Args:
text (str): Text to display.
Returns:
int: Get the number of cells required to display text.
"""
if len(text) < 512:
return _cell_len(text)
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
return total_size
@ -80,7 +90,7 @@ def set_cell_size(text: str, total: int) -> str:
return text + " " * (total - size)
return text[:total]
if not total:
if total <= 0:
return ""
cell_size = cell_len(text)
if cell_size == total:

View File

@ -73,6 +73,8 @@ if TYPE_CHECKING:
from .live import Live
from .status import Status
JUPYTER_DEFAULT_COLUMNS = 115
JUPYTER_DEFAULT_LINES = 100
WINDOWS = platform.system() == "Windows"
HighlighterType = Callable[[Union[str, "Text"]], "Text"]
@ -661,8 +663,18 @@ class Console:
self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter
if self.is_jupyter:
width = width or 93
height = height or 100
if width is None:
jupyter_columns = self._environ.get("JUPYTER_COLUMNS")
if jupyter_columns is not None and jupyter_columns.isdigit():
width = int(jupyter_columns)
else:
width = JUPYTER_DEFAULT_COLUMNS
if height is None:
jupyter_lines = self._environ.get("JUPYTER_LINES")
if jupyter_lines is not None and jupyter_lines.isdigit():
height = int(jupyter_lines)
else:
height = JUPYTER_DEFAULT_LINES
self.soft_wrap = soft_wrap
self._width = width

View File

@ -22,6 +22,8 @@ def report() -> None: # pragma: no cover
"TERM_PROGRAM",
"COLUMNS",
"LINES",
"JUPYTER_COLUMNS",
"JUPYTER_LINES",
"JPY_PARENT_PID",
"VSCODE_VERBOSE_LOGGING",
)

View File

@ -4,6 +4,7 @@ from .align import AlignMethod
from .cells import cell_len, set_cell_size
from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .text import Text
@ -62,10 +63,7 @@ class Rule(JupyterMixin):
chars_len = cell_len(characters)
if not self.title:
rule_text = Text(characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width)
rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text
yield self._rule_line(chars_len, width)
return
if isinstance(self.title, Text):
@ -75,10 +73,16 @@ class Rule(JupyterMixin):
title_text.plain = title_text.plain.replace("\n", " ")
title_text.expand_tabs()
rule_text = Text(end=self.end)
required_space = 4 if self.align == "center" else 2
truncate_width = max(0, width - required_space)
if not truncate_width:
yield self._rule_line(chars_len, width)
return
rule_text = Text(end=self.end)
if self.align == "center":
title_text.truncate(width - 4, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
side_width = (width - cell_len(title_text.plain)) // 2
left = Text(characters * (side_width // chars_len + 1))
left.truncate(side_width - 1)
@ -89,12 +93,12 @@ class Rule(JupyterMixin):
rule_text.append(title_text)
rule_text.append(" " + right.plain, self.style)
elif self.align == "left":
title_text.truncate(width - 2, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(title_text)
rule_text.append(" ")
rule_text.append(characters * (width - rule_text.cell_len), self.style)
elif self.align == "right":
title_text.truncate(width - 2, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
rule_text.append(" ")
rule_text.append(title_text)
@ -102,14 +106,29 @@ class Rule(JupyterMixin):
rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text
def _rule_line(self, chars_len: int, width: int) -> Text:
rule_text = Text(self.characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width)
rule_text.plain = set_cell_size(rule_text.plain, width)
return rule_text
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return Measurement(1, 1)
if __name__ == "__main__": # pragma: no cover
from rich.console import Console
import sys
from rich.console import Console
try:
text = sys.argv[1]
except IndexError:
text = "Hello, World"
console = Console()
console.print(Rule(title=text))
console = Console()
console.print(Rule("foo"), width=4)

View File

@ -59,7 +59,7 @@ class Style:
_bgcolor: Optional[Color]
_attributes: int
_set_attributes: int
_hash: int
_hash: Optional[int]
_null: bool
_meta: Optional[bytes]
@ -190,16 +190,7 @@ class Style:
self._link = link
self._link_id = f"{randint(0, 999999)}" if link else ""
self._meta = None if meta is None else dumps(meta)
self._hash = hash(
(
self._color,
self._bgcolor,
self._attributes,
self._set_attributes,
link,
self._meta,
)
)
self._hash: Optional[int] = None
self._null = not (self._set_attributes or color or bgcolor or link or meta)
@classmethod
@ -227,17 +218,8 @@ class Style:
style._link = None
style._link_id = ""
style._meta = None
style._hash = hash(
(
color,
bgcolor,
None,
None,
None,
None,
)
)
style._null = not (color or bgcolor)
style._hash = None
return style
@classmethod
@ -257,16 +239,7 @@ class Style:
style._link = None
style._link_id = ""
style._meta = dumps(meta)
style._hash = hash(
(
None,
None,
None,
None,
None,
style._meta,
)
)
style._hash = None
style._null = not (meta)
return style
@ -366,6 +339,7 @@ class Style:
Returns:
str: String containing codes.
"""
if self._ansi is None:
sgr: List[str] = []
append = sgr.append
@ -446,16 +420,26 @@ class Style:
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return (
self._color == other._color
and self._bgcolor == other._bgcolor
and self._set_attributes == other._set_attributes
and self._attributes == other._attributes
and self._link == other._link
and self._meta == other._meta
)
return self.__hash__() == other.__hash__()
def __ne__(self, other: Any) -> bool:
if not isinstance(other, Style):
return NotImplemented
return self.__hash__() != other.__hash__()
def __hash__(self) -> int:
if self._hash is not None:
return self._hash
self._hash = hash(
(
self._color,
self._bgcolor,
self._attributes,
self._set_attributes,
self._link,
self._meta,
)
)
return self._hash
@property
@ -502,9 +486,9 @@ class Style:
style._set_attributes = self._set_attributes
style._link = self._link
style._link_id = f"{randint(0, 999999)}" if self._link else ""
style._hash = self._hash
style._null = False
style._meta = None
style._hash = None
return style
@classmethod
@ -677,7 +661,7 @@ class Style:
style._set_attributes = self._set_attributes
style._link = link
style._link_id = f"{randint(0, 999999)}" if link else ""
style._hash = self._hash
style._hash = None
style._null = False
style._meta = self._meta
return style
@ -700,7 +684,7 @@ class Style:
"""
if not text or color_system is None:
return text
attrs = self._make_ansi_codes(color_system)
attrs = self._ansi or self._make_ansi_codes(color_system)
rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
if self._link and not legacy_windows:
rendered = (
@ -720,9 +704,8 @@ class Style:
text = text or str(self)
sys.stdout.write(f"{self.render(text)}\n")
def __add__(self, style: Optional["Style"]) -> "Style":
if not (isinstance(style, Style) or style is None):
return NotImplemented
@lru_cache(maxsize=1024)
def _add(self, style: Optional["Style"]) -> "Style":
if style is None or style._null:
return self
if self._null:
@ -738,14 +721,18 @@ class Style:
new_style._set_attributes = self._set_attributes | style._set_attributes
new_style._link = style._link or self._link
new_style._link_id = style._link_id or self._link_id
new_style._hash = style._hash
new_style._null = style._null
if self._meta and style._meta:
new_style._meta = dumps({**self.meta, **style.meta})
else:
new_style._meta = self._meta or style._meta
new_style._hash = None
return new_style
def __add__(self, style: Optional["Style"]) -> "Style":
combined_style = self._add(style)
return combined_style.copy() if combined_style.link else combined_style
NULL_STYLE = Style()

View File

@ -3,5 +3,30 @@ from rich.console import Console
def test_jupyter():
console = Console(force_jupyter=True)
assert console.width == 93
assert console.width == 115
assert console.height == 100
assert console.color_system == "truecolor"
def test_jupyter_columns_env():
console = Console(_environ={"JUPYTER_COLUMNS": "314"}, force_jupyter=True)
assert console.width == 314
# width take precedence
console = Console(width=40, _environ={"JUPYTER_COLUMNS": "314"}, force_jupyter=True)
assert console.width == 40
# Should not fail
console = Console(
width=40, _environ={"JUPYTER_COLUMNS": "broken"}, force_jupyter=True
)
def test_jupyter_lines_env():
console = Console(_environ={"JUPYTER_LINES": "220"}, force_jupyter=True)
assert console.height == 220
# height take precedence
console = Console(height=40, _environ={"JUPYTER_LINES": "220"}, force_jupyter=True)
assert console.height == 40
# Should not fail
console = Console(
width=40, _environ={"JUPYTER_LINES": "broken"}, force_jupyter=True
)

View File

@ -1,29 +0,0 @@
from __future__ import unicode_literals
from rich._lru_cache import LRUCache
def test_lru_cache():
cache = LRUCache(3)
# insert some values
cache["foo"] = 1
cache["bar"] = 2
cache["baz"] = 3
assert "foo" in cache
# Cache size is 3, so the following should kick oldest one out
cache["egg"] = 4
assert "foo" not in cache
assert "egg" in "egg" in cache
# cache is now full
# look up two keys
cache["bar"]
cache["baz"]
# Insert a new value
cache["eggegg"] = 5
# Check it kicked out the 'oldest' key
assert "egg" not in cache

View File

@ -61,6 +61,47 @@ def test_rule_cjk():
assert console.file.getvalue() == expected
@pytest.mark.parametrize(
"align,outcome",
[
("center", "───\n"),
("left", "… ─\n"),
("right", "─ …\n"),
],
)
def test_rule_not_enough_space_for_title_text(align, outcome):
console = Console(width=3, file=io.StringIO(), record=True)
console.rule("Hello!", align=align)
assert console.file.getvalue() == outcome
def test_rule_center_aligned_title_not_enough_space_for_rule():
console = Console(width=4, file=io.StringIO(), record=True)
console.rule("ABCD")
assert console.file.getvalue() == "────\n"
@pytest.mark.parametrize("align", ["left", "right"])
def test_rule_side_aligned_not_enough_space_for_rule(align):
console = Console(width=2, file=io.StringIO(), record=True)
console.rule("ABCD", align=align)
assert console.file.getvalue() == "──\n"
@pytest.mark.parametrize(
"align,outcome",
[
("center", "─ … ─\n"),
("left", "AB… ─\n"),
("right", "─ AB…\n"),
],
)
def test_rule_just_enough_width_available_for_title(align, outcome):
console = Console(width=5, file=io.StringIO(), record=True)
console.rule("ABCD", align=align)
assert console.file.getvalue() == outcome
def test_characters():
console = Console(
width=16,

View File

@ -0,0 +1,76 @@
import io
from textwrap import dedent
import pytest
from rich import box
from rich.console import Console
from rich.rule import Rule
from rich.table import Table
@pytest.mark.parametrize("expand_kwarg", ({}, {"expand": False}))
def test_rule_in_unexpanded_table(expand_kwarg):
console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={})
table = Table(box=box.ASCII, show_header=False, **expand_kwarg)
table.add_column()
table.add_column()
table.add_row("COL1", "COL2")
table.add_row("COL1", Rule())
table.add_row("COL1", "COL2")
console.print(table)
expected = dedent(
"""\
+-------------+
| COL1 | COL2 |
| COL1 | |
| COL1 | COL2 |
+-------------+
"""
)
result = console.file.getvalue()
assert result == expected
def test_rule_in_expanded_table():
console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={})
table = Table(box=box.ASCII, expand=True, show_header=False)
table.add_column()
table.add_column()
table.add_row("COL1", "COL2")
table.add_row("COL1", Rule(style=None))
table.add_row("COL1", "COL2")
console.print(table)
expected = dedent(
"""\
+------------------------------+
| COL1 | COL2 |
| COL1 | |
| COL1 | COL2 |
+------------------------------+
"""
)
result = console.file.getvalue()
assert result == expected
def test_rule_in_ratio_table():
console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={})
table = Table(box=box.ASCII, expand=True, show_header=False)
table.add_column(ratio=1)
table.add_column()
table.add_row("COL1", "COL2")
table.add_row("COL1", Rule(style=None))
table.add_row("COL1", "COL2")
console.print(table)
expected = dedent(
"""\
+------------------------------+
| COL1 | COL2 |
| COL1 | |
| COL1 | COL2 |
+------------------------------+
"""
)
result = console.file.getvalue()
assert result == expected

View File

@ -46,17 +46,15 @@ def test_spinner_update():
spinner = Spinner("dots")
console.print(spinner)
spinner.update(text="Bar", style="green", speed=2)
time += 80 / 1000
console.print(spinner)
rule = Rule("Bar")
spinner.update(text=Rule("Bar"))
spinner.update(text=rule)
time += 80 / 1000
console.print(spinner)
result = console.end_capture()
print(repr(result))
expected = f"\n\x1b[32m\x1b[0m Bar\n\x1b[32m⠸\x1b[0m \x1b[92m────── \x1b[0mBar\x1b[92m ──────\x1b[0m\n"
expected = "\n\x1b[92m\x1b[0m\n"
assert result == expected

View File

@ -1,7 +1,7 @@
import pytest
from rich.color import Color, ColorSystem, ColorType
from rich import errors
from rich.color import Color, ColorSystem, ColorType
from rich.style import Style, StyleStack
@ -168,7 +168,6 @@ def test_test():
def test_add():
assert Style(color="red") + None == Style(color="red")
assert Style().__add__("foo") == NotImplemented
def test_iadd():