From 49c5e74b370f22496683aa481fb00b84099dc218 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Wed, 24 Jun 2026 22:01:11 +0200 Subject: [PATCH 1/2] fix: clamp n to len(pwcs) in wcswidth/wcstwidth to avoid IndexError Passing n > len(pwcs) to wcswidth() or wcstwidth() causes an IndexError when the loop index reaches the end of the string, because `end` was set to n directly. Fix: `end = min(n, len(pwcs))` so that n larger than the string length is treated the same as measuring the whole string, which is the natural Python semantics for sized strings. Adds a regression test covering ASCII, wide characters, and ZWJ clusters. --- tests/test_core.py | 16 ++++++++++++++++ wcwidth/_wcswidth.py | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 882b2eaf..cb3deba1 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -497,6 +497,22 @@ def test_zwj_at_end_of_string(): assert wcwidth.wcswidth('a\u200D') == 1 +def test_wcswidth_n_exceeds_length(): + """ + wcswidth() with n > len(string) returns same as n=None. + + Passing n larger than the string length should not raise IndexError; + it should behave identically to measuring the whole string. + """ + # ASCII string + assert wcwidth.wcswidth('hello', 10) == 5 + # Wide characters + assert wcwidth.wcswidth('\u30B3\u30F3', 5) == 4 + # ZWJ cluster + family = '\U0001F468\u200D\U0001F469\u200D\U0001F467' + assert wcwidth.wcswidth(family, len(family) + 1) == wcwidth.wcswidth(family) + + def test_soft_hyphen(): # Test SOFT HYPHEN, category 'Cf' usually are zero-width, but most # implementations agree to draw it was '1' cell, visually diff --git a/wcwidth/_wcswidth.py b/wcwidth/_wcswidth.py index 4b473fa7..32488d5b 100644 --- a/wcwidth/_wcswidth.py +++ b/wcwidth/_wcswidth.py @@ -98,7 +98,7 @@ def wcswidth( _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) - end = len(pwcs) if n is None else n + end = len(pwcs) if n is None else min(n, len(pwcs)) total_width = 0 idx = 0 @@ -262,7 +262,7 @@ def wcstwidth( # Select wcwidth call pattern for best lru_cache performance _wcwidth = wcwidth if ambiguous_width == 1 else lambda c: wcwidth(c, 'auto', ambiguous_width) - end = len(pwcs) if n is None else n + end = len(pwcs) if n is None else min(n, len(pwcs)) total_width = 0 idx = 0 From ab680a8859a7ef2b738a3913d9fec34d72366091 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 17:23:22 +0200 Subject: [PATCH 2/2] Cover wcswidth n overflow companion paths --- tests/test_core.py | 7 ++++--- tests/test_term_overrides.py | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index cb3deba1..4b22edc2 100755 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -499,10 +499,10 @@ def test_zwj_at_end_of_string(): def test_wcswidth_n_exceeds_length(): """ - wcswidth() with n > len(string) returns same as n=None. + Wcswidth() with n > len(string) returns same as n=None. - Passing n larger than the string length should not raise IndexError; - it should behave identically to measuring the whole string. + Passing n larger than the string length should not raise IndexError; it should behave + identically to measuring the whole string. """ # ASCII string assert wcwidth.wcswidth('hello', 10) == 5 @@ -511,6 +511,7 @@ def test_wcswidth_n_exceeds_length(): # ZWJ cluster family = '\U0001F468\u200D\U0001F469\u200D\U0001F467' assert wcwidth.wcswidth(family, len(family) + 1) == wcwidth.wcswidth(family) + assert wcwidth.wcstwidth(family, len(family) + 1) == wcwidth.wcstwidth(family) def test_soft_hyphen(): diff --git a/tests/test_term_overrides.py b/tests/test_term_overrides.py index 75f88052..2c320005 100644 --- a/tests/test_term_overrides.py +++ b/tests/test_term_overrides.py @@ -418,6 +418,13 @@ def test_get_term_overrides_narrow_wider_still_empty(): assert overrides.narrow_wider == () +@pytest.mark.parametrize('func', [wcwidth.wcstwidth, wcwidth.width]) +def test_narrow_wider_width(func): + """Width() matches wcstwidth() for narrow_wider overrides.""" + assert wcwidth.wcswidth('\u261d') == 1 + assert func('\u261d', term_program='kitty') == 2 + + @pytest.mark.parametrize('codepoint', [ '\u00ad', '\u0600',