diff --git a/CHANGELOG.md b/CHANGELOG.md index 05762d06..8be1bf54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5562f14e..8a121b28 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -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) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6688094b..ce471371 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -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 diff --git a/docs/source/console.rst b/docs/source/console.rst index 9560aa16..77ca6fc7 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -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. diff --git a/poetry.lock b/poetry.lock index e3b7d8d7..5def1f39 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index f2edc90a..c9f80083 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py deleted file mode 100644 index a2278943..00000000 --- a/rich/_lru_cache.py +++ /dev/null @@ -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 diff --git a/rich/cells.py b/rich/cells.py index 834c3710..020e14f6 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -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: diff --git a/rich/console.py b/rich/console.py index 8942a1be..4ccaa017 100644 --- a/rich/console.py +++ b/rich/console.py @@ -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 diff --git a/rich/diagnose.py b/rich/diagnose.py index 09c4e062..91e55bd8 100644 --- a/rich/diagnose.py +++ b/rich/diagnose.py @@ -22,6 +22,8 @@ def report() -> None: # pragma: no cover "TERM_PROGRAM", "COLUMNS", "LINES", + "JUPYTER_COLUMNS", + "JUPYTER_LINES", "JPY_PARENT_PID", "VSCODE_VERBOSE_LOGGING", ) diff --git a/rich/rule.py b/rich/rule.py index d14394f2..f1159524 100644 --- a/rich/rule.py +++ b/rich/rule.py @@ -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) diff --git a/rich/style.py b/rich/style.py index 17b1ace8..b2e8aff7 100644 --- a/rich/style.py +++ b/rich/style.py @@ -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() diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index a69f211b..7c327e62 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -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 + ) diff --git a/tests/test_lrucache.py b/tests/test_lrucache.py deleted file mode 100644 index 3cb1e298..00000000 --- a/tests/test_lrucache.py +++ /dev/null @@ -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 diff --git a/tests/test_rule.py b/tests/test_rule.py index 571734e3..398c7558 100644 --- a/tests/test_rule.py +++ b/tests/test_rule.py @@ -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, diff --git a/tests/test_rule_in_table.py b/tests/test_rule_in_table.py new file mode 100644 index 00000000..bc911391 --- /dev/null +++ b/tests/test_rule_in_table.py @@ -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 diff --git a/tests/test_spinner.py b/tests/test_spinner.py index 98e3d9d6..efeeb717 100644 --- a/tests/test_spinner.py +++ b/tests/test_spinner.py @@ -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 diff --git a/tests/test_style.py b/tests/test_style.py index 7c17dc3c..bf02af83 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -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():