diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b093843a3..d4233f00e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ 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 + +### Fixed +- Fixed `cells.split_graphemes` infinite loop. https://github.com/Textualize/rich/pull/4002 + ## [14.3.2] - 2026-02-01 ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4d77a0e3ed..3c340c9396 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -100,3 +100,4 @@ The following people have contributed to the development of Rich: - [Brandon Capener](https://github.com/bcapener) - [Alex Zheng](https://github.com/alexzheng111) - [Sebastian Speitel](https://github.com/SebastianSpeitel) +- [Jeff Weiss](https://github.com/jmw182) diff --git a/poetry.lock b/poetry.lock index 0234516312..26b275a227 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "appnope" @@ -804,6 +804,21 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, + {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "pyyaml" version = "6.0.3" @@ -989,6 +1004,7 @@ files = [ {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] +markers = {main = "extra == \"jupyter\" and python_version < \"3.10\""} [[package]] name = "virtualenv" @@ -1044,4 +1060,4 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "2.1" python-versions = ">=3.8.0" -content-hash = "610597849eb5fb1d82cd5e647d616dd41412674d531c2f7819380bbc9b54e8e2" +content-hash = "4927e5841e58c478fd991cf8d660f9968373873c1b20bdbc597cff69eae2468e" diff --git a/pyproject.toml b/pyproject.toml index 23e86e4006..ac2b3f74bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ pytest-cov = "^3.0.0" attrs = "^21.4.0" pre-commit = "^2.17.0" typing-extensions = ">=4.0.0, <5.0" +pytest-timeout = "^2.4.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/rich/cells.py b/rich/cells.py index 31165957b9..904515881c 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -207,6 +207,9 @@ def split_graphemes( # zero width characters are associated with the previous character start, _end, cell_length = spans[-1] spans[-1] = (start, index := index + 1, cell_length) + else: + # avoid infinite loop (zero width character before first span) + index += 1 return (spans, total_width) diff --git a/tests/test_cells.py b/tests/test_cells.py index f101740a01..186940be5e 100644 --- a/tests/test_cells.py +++ b/tests/test_cells.py @@ -204,3 +204,15 @@ def test_non_printable(): for ordinal in range(31): character = chr(ordinal) assert cell_len(character) == 0 + + +@pytest.mark.timeout(2) +def test_non_printable_at_grapheme_start(): + """Test non printable characters + tests regression in 14.3.2 https://github.com/Textualize/rich/issues/3958 + """ + text = "\x1b\x1b" + # we are mostly just testing that this does not hang + spans, cell_length = split_graphemes(text) + assert spans == [] + assert cell_length == 0