From d9a0d479464d27ee180aad8297774775e26e764e Mon Sep 17 00:00:00 2001 From: Andy Gimblett Date: Tue, 24 May 2022 13:34:56 +0100 Subject: [PATCH 01/20] Add Rule.__rich_measure__ and tests vs behaviour in tables See [issue #2266](https://github.com/Textualize/rich/issues/2266) --- rich/rule.py | 6 +++ tests/test_rule_in_table.py | 76 +++++++++++++++++++++++++++++++++++++ tests/test_spinner.py | 1 + 3 files changed, 83 insertions(+) create mode 100644 tests/test_rule_in_table.py diff --git a/rich/rule.py b/rich/rule.py index d14394f2..6c25e371 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 @@ -102,6 +103,11 @@ class Rule(JupyterMixin): rule_text.plain = set_cell_size(rule_text.plain, width) yield 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 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..2753eeab 100644 --- a/tests/test_spinner.py +++ b/tests/test_spinner.py @@ -34,6 +34,7 @@ def test_spinner_render(): assert result == expected +@pytest.mark.skip(reason="Broken with Rule.__rich__measure in place") def test_spinner_update(): time = 0.0 From b4346ba77978a9798505fbc6b55fd4398009b5d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 13:39:10 +0000 Subject: [PATCH 02/20] Bump mypy from 0.950 to 0.961 Bumps [mypy](https://github.com/python/mypy) from 0.950 to 0.961. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.950...v0.961) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- poetry.lock | 50 +++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index 30edc19b..0d7091f2 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 = [ @@ -1424,29 +1424,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" From 067f20d42047155e1c7ebb27ede052e94490a782 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Thu, 9 Jun 2022 14:07:02 +0100 Subject: [PATCH 03/20] Use environment variables for Jupyter width/height --- rich/console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index eba27611..f87945d6 100644 --- a/rich/console.py +++ b/rich/console.py @@ -656,8 +656,8 @@ 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 + width = width or int(self._environ.get("COLUMNS", 93)) + height = height or int(self._environ.get("LINES", 100)) self.soft_wrap = soft_wrap self._width = width From b0fb18a439e062f911acf44482eb80fef5729491 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Thu, 9 Jun 2022 14:08:16 +0100 Subject: [PATCH 04/20] Do with another or --- rich/console.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index f87945d6..c35d3558 100644 --- a/rich/console.py +++ b/rich/console.py @@ -656,8 +656,8 @@ class Console: self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter if self.is_jupyter: - width = width or int(self._environ.get("COLUMNS", 93)) - height = height or int(self._environ.get("LINES", 100)) + width = width or int(self._environ.get("COLUMNS")) or 93 + height = height or int(self._environ.get("LINES")) or 100 self.soft_wrap = soft_wrap self._width = width From da10ca92fcc4c66890339d3f942dcd0f356d8da5 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Thu, 9 Jun 2022 22:52:37 +0100 Subject: [PATCH 05/20] Add JUPYTER_COLUMNS/JUPYTER_LINES and tests --- rich/console.py | 6 ++++-- tests/test_jupyter.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/rich/console.py b/rich/console.py index c35d3558..e42053c3 100644 --- a/rich/console.py +++ b/rich/console.py @@ -656,8 +656,10 @@ class Console: self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter if self.is_jupyter: - width = width or int(self._environ.get("COLUMNS")) or 93 - height = height or int(self._environ.get("LINES")) or 100 + jupyter_columns = self._environ.get("JUPYTER_COLUMNS", "") + width = width or (int(jupyter_columns) if jupyter_columns.isdigit() else 93) + jupyter_lines = self._environ.get("JUPYTER_LINES", "") + height = height or (int(jupyter_lines) if jupyter_lines.isdigit() else 100) self.soft_wrap = soft_wrap self._width = width diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index a69f211b..df244060 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -4,4 +4,25 @@ from rich.console import Console def test_jupyter(): console = Console(force_jupyter=True) assert console.width == 93 + 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) \ No newline at end of file From d4f9f6dc50e3dc94fbb0af12341b0d77dcc065db Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Thu, 9 Jun 2022 23:07:28 +0100 Subject: [PATCH 06/20] Update column width, CHANGELOG, CONTRIBUTORS --- CHANGELOG.md | 8 ++++++++ CONTRIBUTORS.md | 1 + rich/console.py | 4 +++- tests/test_jupyter.py | 10 +++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70652e14..02f49528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 - Fix text wrapping edge case https://github.com/Textualize/rich/pull/2296 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c24f21b1..0c150113 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -24,6 +24,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/rich/console.py b/rich/console.py index e42053c3..1ea40faf 100644 --- a/rich/console.py +++ b/rich/console.py @@ -657,7 +657,9 @@ class Console: self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter if self.is_jupyter: jupyter_columns = self._environ.get("JUPYTER_COLUMNS", "") - width = width or (int(jupyter_columns) if jupyter_columns.isdigit() else 93) + width = width or ( + int(jupyter_columns) if jupyter_columns.isdigit() else 115 + ) jupyter_lines = self._environ.get("JUPYTER_LINES", "") height = height or (int(jupyter_lines) if jupyter_lines.isdigit() else 100) diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index df244060..7c327e62 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -3,7 +3,7 @@ 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" @@ -15,7 +15,9 @@ def test_jupyter_columns_env(): 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) + console = Console( + width=40, _environ={"JUPYTER_COLUMNS": "broken"}, force_jupyter=True + ) def test_jupyter_lines_env(): @@ -25,4 +27,6 @@ def test_jupyter_lines_env(): 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) \ No newline at end of file + console = Console( + width=40, _environ={"JUPYTER_LINES": "broken"}, force_jupyter=True + ) From 29294b82d66e6511a82cdb246e034fd390226449 Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Thu, 9 Jun 2022 23:13:31 +0100 Subject: [PATCH 07/20] Add to docs --- docs/source/console.rst | 2 ++ rich/diagnose.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/source/console.rst b/docs/source/console.rst index d3df2828..c6fc8d84 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -421,3 +421,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/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", ) From dbb27d30a7fde4f888506b7cbba79301344fedb5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 15 Jun 2022 14:49:15 +0100 Subject: [PATCH 08/20] Fix hang when Rule title greater than required space --- rich/cells.py | 2 +- rich/rule.py | 31 ++++++++++++++++++++++--------- tests/test_rule.py | 41 +++++++++++++++++++++++++++++++++++++++++ tests/test_spinner.py | 9 +++------ 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/rich/cells.py b/rich/cells.py index d7adf5a0..6d62bf6f 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -80,7 +80,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/rule.py b/rich/rule.py index 6c25e371..f1159524 100644 --- a/rich/rule.py +++ b/rich/rule.py @@ -63,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): @@ -76,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) @@ -90,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) @@ -103,6 +106,12 @@ 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: @@ -110,12 +119,16 @@ class Rule(JupyterMixin): 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/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_spinner.py b/tests/test_spinner.py index 2753eeab..efeeb717 100644 --- a/tests/test_spinner.py +++ b/tests/test_spinner.py @@ -34,7 +34,6 @@ def test_spinner_render(): assert result == expected -@pytest.mark.skip(reason="Broken with Rule.__rich__measure in place") def test_spinner_update(): time = 0.0 @@ -47,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 From 9332476dbd15fbc489806e38a38fe5c73bc8c0f0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 15 Jun 2022 15:04:31 +0100 Subject: [PATCH 09/20] Update contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5562f14e..79c03094 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) From f0b23ac3f0debfab7516ff0cb2a3d1acf8ea86fa Mon Sep 17 00:00:00 2001 From: Antony Milne Date: Wed, 15 Jun 2022 17:49:48 +0100 Subject: [PATCH 10/20] Change according to review --- rich/console.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/rich/console.py b/rich/console.py index 1ea40faf..da6becb8 100644 --- a/rich/console.py +++ b/rich/console.py @@ -72,6 +72,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"] @@ -656,12 +658,18 @@ class Console: self.is_jupyter = _is_jupyter() if force_jupyter is None else force_jupyter if self.is_jupyter: - jupyter_columns = self._environ.get("JUPYTER_COLUMNS", "") - width = width or ( - int(jupyter_columns) if jupyter_columns.isdigit() else 115 - ) - jupyter_lines = self._environ.get("JUPYTER_LINES", "") - height = height or (int(jupyter_lines) if jupyter_lines.isdigit() else 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 From a177fb525e3d558c68f955d5296e60189c2dc5f7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 15 Jun 2022 20:56:47 +0100 Subject: [PATCH 11/20] fix hash --- rich/style.py | 72 +++++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/rich/style.py b/rich/style.py index 17b1ace8..e757e314 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,9 +661,10 @@ 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 def render( @@ -700,7 +685,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,6 +705,7 @@ class Style: text = text or str(self) sys.stdout.write(f"{self.render(text)}\n") + @lru_cache(maxsize=4096) def __add__(self, style: Optional["Style"]) -> "Style": if not (isinstance(style, Style) or style is None): return NotImplemented @@ -738,12 +724,12 @@ 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 From da790be8efb0c360a54683e3c5c67bb8ad34c058 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 15 Jun 2022 21:01:42 +0100 Subject: [PATCH 12/20] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88e04594..24306ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow exceptions that are raised while a Live is rendered to be displayed and/or processed https://github.com/Textualize/rich/pull/2305 - 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 +- Fixed hash issue in Styles class https://github.com/Textualize/rich/pull/2346 ## [12.4.4] - 2022-05-24 From 516a00e29b261e0cea67022ee04f74a9f24c257b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 15 Jun 2022 21:02:56 +0100 Subject: [PATCH 13/20] whitespace --- rich/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rich/style.py b/rich/style.py index e757e314..9f590478 100644 --- a/rich/style.py +++ b/rich/style.py @@ -664,7 +664,6 @@ class Style: style._hash = None style._null = False style._meta = self._meta - return style def render( From 7cc021d47cdd0079d91c5ed12099085ff840bf21 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 17 Jun 2022 10:46:23 +0100 Subject: [PATCH 14/20] new lru cache implementation --- rich/_lru_cache.py | 120 +++++++++++++++++++++++++++++++++-------- rich/style.py | 11 +++- tests/test_lrucache.py | 34 +++++++++++- 3 files changed, 141 insertions(+), 24 deletions(-) diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py index a2278943..1d423fdc 100644 --- a/rich/_lru_cache.py +++ b/rich/_lru_cache.py @@ -1,38 +1,116 @@ -from typing import Dict, Generic, TypeVar, TYPE_CHECKING -import sys +from threading import Lock +from typing import Generic, List, Optional, TypeVar, Union, overload CacheKey = TypeVar("CacheKey") CacheValue = TypeVar("CacheValue") - -if sys.version_info < (3, 9): - from typing_extensions import OrderedDict -else: - from collections import OrderedDict +DefaultValue = TypeVar("DefaultValue") -class LRUCache(OrderedDict[CacheKey, CacheValue]): +class LRUCache(Generic[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. + The implementation is similar to functools.lru_cache, which uses a linked + list to keep track of the most recently used items. + + Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference + to the previous entry, and NEXT is a reference to the next value. + """ - def __init__(self, cache_size: int) -> None: - self.cache_size = cache_size + def __init__(self, maxsize: int) -> None: + self.maxsize = maxsize + self.cache: dict[CacheKey, List[object]] = {} + self.full = False + self.root: List[object] = [] + self._lock = Lock() 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 __len__(self) -> int: + return len(self.cache) + + def set(self, key: CacheKey, value: CacheValue) -> None: + """Set a value. + + Args: + key (CacheKey): Key. + value (CacheValue): Value. + """ + with self._lock: + link = self.cache.get(key) + if link is None: + root = self.root + if not root: + self.root[:] = [self.root, self.root, key, value] + else: + self.root = [root[0], root, key, value] + root[0][1] = self.root # type: ignore[index] + root[0] = self.root + self.cache[key] = self.root + + if self.full or len(self.cache) > self.maxsize: + self.full = True + root = self.root + last = root[0] + last[0][1] = root # type: ignore[index] + root[0] = last[0] # type: ignore[index] + del self.cache[last[2]] # type: ignore[index] + + __setitem__ = set + + @overload + def get(self, key: CacheKey) -> Optional[CacheValue]: + ... + + @overload + def get( + self, key: CacheKey, default: DefaultValue + ) -> Union[CacheValue, DefaultValue]: + ... + + def get( + self, key: CacheKey, default: Optional[DefaultValue] = None + ) -> Union[CacheValue, Optional[DefaultValue]]: + """Get a value from the cache, or return a default if the key is not present. + + Args: + key (CacheKey): Key + default (Optional[DefaultValue], optional): Default to return if key is not present. Defaults to None. + + Returns: + Union[CacheValue, Optional[DefaultValue]]: Either the value or a default. + """ + link = self.cache.get(key) + if link is None: + return default + if link is not self.root: + with self._lock: + link[0][1] = link[1] # type: ignore[index] + link[1][0] = link[0] # type: ignore[index] + root = self.root + link[0] = root[0] + link[1] = root + root[0][1] = link # type: ignore[index] + root[0] = link + self.root = link + return link[3] # type: ignore[return-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 + link = self.cache[key] + if link is not self.root: + with self._lock: + link[0][1] = link[1] # type: ignore[index] + link[1][0] = link[0] # type: ignore[index] + root = self.root + link[0] = root[0] + link[1] = root + root[0][1] = link # type: ignore[index] + root[0] = link + self.root = link + return link[3] # type: ignore[return-value] + + def __contains__(self, key: CacheKey) -> bool: + return key in self.cache diff --git a/rich/style.py b/rich/style.py index 9f590478..0f5831aa 100644 --- a/rich/style.py +++ b/rich/style.py @@ -5,6 +5,7 @@ from random import randint from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast from . import errors +from ._lru_cache import LRUCache from .color import Color, ColorParseError, ColorSystem, blend_rgb from .repr import Result, rich_repr from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme @@ -119,6 +120,9 @@ class Style: "o": "overline", } + # Caches results of Style.__add__ + _add_cache: LRUCache[tuple["Style", Optional["Style"]], "Style"] = LRUCache(1024) + def __init__( self, *, @@ -704,8 +708,12 @@ class Style: text = text or str(self) sys.stdout.write(f"{self.render(text)}\n") - @lru_cache(maxsize=4096) def __add__(self, style: Optional["Style"]) -> "Style": + + cache_key = (self, style) + cached_style = self._add_cache.get(cache_key) + if cached_style is not None: + return cached_style.copy() if cached_style.link else cached_style if not (isinstance(style, Style) or style is None): return NotImplemented if style is None or style._null: @@ -729,6 +737,7 @@ class Style: else: new_style._meta = self._meta or style._meta new_style._hash = None + self._add_cache[cache_key] = new_style return new_style diff --git a/tests/test_lrucache.py b/tests/test_lrucache.py index 3cb1e298..76ba5a67 100644 --- a/tests/test_lrucache.py +++ b/tests/test_lrucache.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals - from rich._lru_cache import LRUCache @@ -12,11 +11,13 @@ def test_lru_cache(): cache["bar"] = 2 cache["baz"] = 3 assert "foo" in cache + assert "bar" in cache + assert "baz" 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 + assert "egg" in cache # cache is now full # look up two keys @@ -25,5 +26,34 @@ def test_lru_cache(): # Insert a new value cache["eggegg"] = 5 + assert len(cache) == 3 # Check it kicked out the 'oldest' key assert "egg" not in cache + assert "eggegg" in cache + + +def test_lru_cache_get(): + 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 len(cache) == 3 + assert cache.get("foo") is None + assert "egg" in cache + + # cache is now full + # look up two keys + cache.get("bar") + cache.get("baz") + + # Insert a new value + cache["eggegg"] = 5 + # Check it kicked out the 'oldest' key + assert "egg" not in cache + assert "eggegg" in cache From c8a37395394cbc6fc921c5db329655ca02375d15 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 17 Jun 2022 10:48:28 +0100 Subject: [PATCH 15/20] whitespace --- rich/style.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rich/style.py b/rich/style.py index 0f5831aa..867559f6 100644 --- a/rich/style.py +++ b/rich/style.py @@ -709,7 +709,6 @@ class Style: sys.stdout.write(f"{self.render(text)}\n") def __add__(self, style: Optional["Style"]) -> "Style": - cache_key = (self, style) cached_style = self._add_cache.get(cache_key) if cached_style is not None: From 017c9e39b4dfc112ee3701789bb1bd7459489fa5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 17 Jun 2022 10:52:21 +0100 Subject: [PATCH 16/20] fix order --- rich/cells.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rich/cells.py b/rich/cells.py index 834c3710..77cae8c2 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -1,6 +1,6 @@ import re from functools import lru_cache -from typing import Dict, List +from typing import List from ._cell_widths import CELL_WIDTHS from ._lru_cache import LRUCache @@ -9,7 +9,7 @@ from ._lru_cache import LRUCache _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: +def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024 * 4)) -> int: """Get the number of cells required to display text. Args: From 3bc9b287e7865f81f1c61593d040e37e333d042f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 17 Jun 2022 10:57:31 +0100 Subject: [PATCH 17/20] type fixes --- rich/_lru_cache.py | 4 ++-- rich/style.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py index 1d423fdc..46e775f0 100644 --- a/rich/_lru_cache.py +++ b/rich/_lru_cache.py @@ -1,5 +1,5 @@ from threading import Lock -from typing import Generic, List, Optional, TypeVar, Union, overload +from typing import Dict, Generic, List, Optional, TypeVar, Union, overload CacheKey = TypeVar("CacheKey") CacheValue = TypeVar("CacheValue") @@ -23,7 +23,7 @@ class LRUCache(Generic[CacheKey, CacheValue]): def __init__(self, maxsize: int) -> None: self.maxsize = maxsize - self.cache: dict[CacheKey, List[object]] = {} + self.cache: Dict[CacheKey, List[object]] = {} self.full = False self.root: List[object] = [] self._lock = Lock() diff --git a/rich/style.py b/rich/style.py index 867559f6..c4b43282 100644 --- a/rich/style.py +++ b/rich/style.py @@ -2,7 +2,7 @@ import sys from functools import lru_cache from marshal import dumps, loads from random import randint -from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast +from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union, cast from . import errors from ._lru_cache import LRUCache @@ -121,7 +121,7 @@ class Style: } # Caches results of Style.__add__ - _add_cache: LRUCache[tuple["Style", Optional["Style"]], "Style"] = LRUCache(1024) + _add_cache: LRUCache[Tuple["Style", Optional["Style"]], "Style"] = LRUCache(1024) def __init__( self, From b4a441d3e32e3c1b8ed8acb00eed86b7c10f3f7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jun 2022 10:08:28 +0000 Subject: [PATCH 18/20] Bump sphinx from 4.5.0 to 5.0.2 Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.5.0 to 5.0.2. - [Release notes](https://github.com/sphinx-doc/sphinx/releases) - [Changelog](https://github.com/sphinx-doc/sphinx/blob/5.x/CHANGES) - [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.5.0...v5.0.2) --- updated-dependencies: - dependency-name: sphinx dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d4e55f7629236085241ec46109631c010bd313dc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 20 Jun 2022 11:19:45 +0100 Subject: [PATCH 19/20] imports --- CHANGELOG.md | 5 ++ rich/_lru_cache.py | 116 ----------------------------------------- rich/cells.py | 28 ++++++---- rich/style.py | 20 +++---- tests/test_lrucache.py | 59 --------------------- tests/test_style.py | 3 +- 6 files changed, 32 insertions(+), 199 deletions(-) delete mode 100644 rich/_lru_cache.py delete mode 100644 tests/test_lrucache.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6888cd36..4ffbe8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 + ## [12.4.4] - 2022-05-24 ### Changed diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py deleted file mode 100644 index 46e775f0..00000000 --- a/rich/_lru_cache.py +++ /dev/null @@ -1,116 +0,0 @@ -from threading import Lock -from typing import Dict, Generic, List, Optional, TypeVar, Union, overload - -CacheKey = TypeVar("CacheKey") -CacheValue = TypeVar("CacheValue") -DefaultValue = TypeVar("DefaultValue") - - -class LRUCache(Generic[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. - - The implementation is similar to functools.lru_cache, which uses a linked - list to keep track of the most recently used items. - - Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference - to the previous entry, and NEXT is a reference to the next value. - - """ - - def __init__(self, maxsize: int) -> None: - self.maxsize = maxsize - self.cache: Dict[CacheKey, List[object]] = {} - self.full = False - self.root: List[object] = [] - self._lock = Lock() - super().__init__() - - def __len__(self) -> int: - return len(self.cache) - - def set(self, key: CacheKey, value: CacheValue) -> None: - """Set a value. - - Args: - key (CacheKey): Key. - value (CacheValue): Value. - """ - with self._lock: - link = self.cache.get(key) - if link is None: - root = self.root - if not root: - self.root[:] = [self.root, self.root, key, value] - else: - self.root = [root[0], root, key, value] - root[0][1] = self.root # type: ignore[index] - root[0] = self.root - self.cache[key] = self.root - - if self.full or len(self.cache) > self.maxsize: - self.full = True - root = self.root - last = root[0] - last[0][1] = root # type: ignore[index] - root[0] = last[0] # type: ignore[index] - del self.cache[last[2]] # type: ignore[index] - - __setitem__ = set - - @overload - def get(self, key: CacheKey) -> Optional[CacheValue]: - ... - - @overload - def get( - self, key: CacheKey, default: DefaultValue - ) -> Union[CacheValue, DefaultValue]: - ... - - def get( - self, key: CacheKey, default: Optional[DefaultValue] = None - ) -> Union[CacheValue, Optional[DefaultValue]]: - """Get a value from the cache, or return a default if the key is not present. - - Args: - key (CacheKey): Key - default (Optional[DefaultValue], optional): Default to return if key is not present. Defaults to None. - - Returns: - Union[CacheValue, Optional[DefaultValue]]: Either the value or a default. - """ - link = self.cache.get(key) - if link is None: - return default - if link is not self.root: - with self._lock: - link[0][1] = link[1] # type: ignore[index] - link[1][0] = link[0] # type: ignore[index] - root = self.root - link[0] = root[0] - link[1] = root - root[0][1] = link # type: ignore[index] - root[0] = link - self.root = link - return link[3] # type: ignore[return-value] - - def __getitem__(self, key: CacheKey) -> CacheValue: - link = self.cache[key] - if link is not self.root: - with self._lock: - link[0][1] = link[1] # type: ignore[index] - link[1][0] = link[0] # type: ignore[index] - root = self.root - link[0] = root[0] - link[1] = root - root[0][1] = link # type: ignore[index] - root[0] = link - self.root = link - return link[3] # type: ignore[return-value] - - def __contains__(self, key: CacheKey) -> bool: - return key in self.cache diff --git a/rich/cells.py b/rich/cells.py index 3278b6fa..020e14f6 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -1,15 +1,15 @@ import re from functools import lru_cache -from typing import 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: LRUCache[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: LRUCache[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 diff --git a/rich/style.py b/rich/style.py index c4b43282..b2e8aff7 100644 --- a/rich/style.py +++ b/rich/style.py @@ -2,10 +2,9 @@ import sys from functools import lru_cache from marshal import dumps, loads from random import randint -from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, Union, cast +from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast from . import errors -from ._lru_cache import LRUCache from .color import Color, ColorParseError, ColorSystem, blend_rgb from .repr import Result, rich_repr from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme @@ -120,9 +119,6 @@ class Style: "o": "overline", } - # Caches results of Style.__add__ - _add_cache: LRUCache[Tuple["Style", Optional["Style"]], "Style"] = LRUCache(1024) - def __init__( self, *, @@ -708,13 +704,8 @@ class Style: text = text or str(self) sys.stdout.write(f"{self.render(text)}\n") - def __add__(self, style: Optional["Style"]) -> "Style": - cache_key = (self, style) - cached_style = self._add_cache.get(cache_key) - if cached_style is not None: - return cached_style.copy() if cached_style.link else cached_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: @@ -736,9 +727,12 @@ class Style: else: new_style._meta = self._meta or style._meta new_style._hash = None - self._add_cache[cache_key] = new_style 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_lrucache.py b/tests/test_lrucache.py deleted file mode 100644 index 76ba5a67..00000000 --- a/tests/test_lrucache.py +++ /dev/null @@ -1,59 +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 - assert "bar" in cache - assert "baz" 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 cache - - # cache is now full - # look up two keys - cache["bar"] - cache["baz"] - - # Insert a new value - cache["eggegg"] = 5 - assert len(cache) == 3 - # Check it kicked out the 'oldest' key - assert "egg" not in cache - assert "eggegg" in cache - - -def test_lru_cache_get(): - 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 len(cache) == 3 - assert cache.get("foo") is None - assert "egg" in cache - - # cache is now full - # look up two keys - cache.get("bar") - cache.get("baz") - - # Insert a new value - cache["eggegg"] = 5 - # Check it kicked out the 'oldest' key - assert "egg" not in cache - assert "eggegg" in cache 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(): From f40de7b70cb7e7636b12320b6f0f55449441a2ed Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 20 Jun 2022 11:24:00 +0100 Subject: [PATCH 20/20] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ffbe8fe..f2c8eeb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 ### Fixed