summaryrefslogtreecommitdiffstats
path: root/contrib/python/wcwidth/py3/tests
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/python/wcwidth/py3/tests')
-rw-r--r--contrib/python/wcwidth/py3/tests/conftest.py21
-rw-r--r--contrib/python/wcwidth/py3/tests/test_benchmarks.py86
-rw-r--r--contrib/python/wcwidth/py3/tests/test_clip.py4
-rw-r--r--contrib/python/wcwidth/py3/tests/test_core.py65
-rw-r--r--contrib/python/wcwidth/py3/tests/test_term_overrides.py597
-rw-r--r--contrib/python/wcwidth/py3/tests/test_text_sizing.py3
-rw-r--r--contrib/python/wcwidth/py3/tests/test_width.py6
7 files changed, 766 insertions, 16 deletions
diff --git a/contrib/python/wcwidth/py3/tests/conftest.py b/contrib/python/wcwidth/py3/tests/conftest.py
index ecbbdc876e0..dbaaed8faa5 100644
--- a/contrib/python/wcwidth/py3/tests/conftest.py
+++ b/contrib/python/wcwidth/py3/tests/conftest.py
@@ -1,8 +1,29 @@
"""Pytest configuration and fixtures."""
+# std imports
+import os
+
# 3rd party
import pytest
+# local
+from wcwidth._constants import resolve_terminal
+
+
[email protected](autouse=True)
+def _clear_resolve_terminal_cache():
+ """Clear resolve_terminal cache and unset TERM/TERM_PROGRAM before each test."""
+ saved_term = os.environ.pop('TERM', None)
+ saved_tprog = os.environ.pop('TERM_PROGRAM', None)
+ resolve_terminal.cache_clear()
+ yield
+ resolve_terminal.cache_clear()
+ if saved_term is not None:
+ os.environ['TERM'] = saved_term
+ if saved_tprog is not None:
+ os.environ['TERM_PROGRAM'] = saved_tprog
+
+
try:
# 3rd party
from pytest_codspeed import BenchmarkFixture # noqa: F401 pylint:disable=unused-import
diff --git a/contrib/python/wcwidth/py3/tests/test_benchmarks.py b/contrib/python/wcwidth/py3/tests/test_benchmarks.py
index 80c9be01de6..7afcb2c0a5c 100644
--- a/contrib/python/wcwidth/py3/tests/test_benchmarks.py
+++ b/contrib/python/wcwidth/py3/tests/test_benchmarks.py
@@ -10,8 +10,9 @@ import pytest
# local
import wcwidth
-
-_width_module = sys.modules['wcwidth._width']
+# Access via import to trigger lazy-load on Python 3.15+; on older
+# Python _width is already in sys.modules and this is a no-op.
+import wcwidth._width as _width_module # noqa: E402
def test_wcwidth_ascii(benchmark):
@@ -605,3 +606,84 @@ def test_ljust_udhr_lines(benchmark):
benchmark.pedantic(lambda: [wcwidth.ljust(line, w + 1, UDHR_FILLCHAR)
for line, w in zip(UDHR_LINES, UDHR_WIDTHS)],
rounds=1, iterations=1)
+
+
+_TERM_PROGRAMS = [
+ 'ghostty',
+ 'xterm.js',
+]
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_term_program(benchmark, term_program):
+ """Benchmark wcstwidth() with term_program (ghostty=0 overrides vs xterm.js=237)."""
+ text = 'Hello δΈ–η•Œ πŸ˜€ cafΓ© ' * 20
+ benchmark(wcwidth.wcstwidth, text, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_ri_term_program(benchmark, term_program):
+ """Benchmark wcstwidth() with RI flags and term_program."""
+ benchmark(wcwidth.wcstwidth, RI_FLAGS_PAIRED, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_emoji_term_program(benchmark, term_program):
+ """Benchmark wcstwidth() with emoji ZWJ sequences and term_program."""
+ text = 'πŸ‘¨\u200dπŸ‘©\u200dπŸ‘§\u200dπŸ‘¦' * 20
+ benchmark(wcwidth.wcstwidth, text, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_wide_term_program(benchmark, term_program):
+ """Benchmark wcstwidth() with wide CJK and term_program."""
+ text = 'コンニチハ、セカむ!' * 20
+ benchmark(wcwidth.wcstwidth, text, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_width_term_program(benchmark, term_program):
+ """Benchmark width() with term_program (ghostty=0 overrides vs xterm.js=237)."""
+ text = 'Hello δΈ–η•Œ πŸ˜€ cafΓ© ' * 20
+ benchmark(wcwidth.width, text, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_width_ri_term_program(benchmark, term_program):
+ """Benchmark width() with RI flags and term_program."""
+ benchmark(wcwidth.width, RI_FLAGS_PAIRED, term_program=term_program)
+
+
+# VS16/VS15-heavy text to exercise the vs16_narrower/vs15_wider bisearch paths
+_VS16_TEXT = ('\u263A\uFE0F' # WHITE SMILING FACE + VS16
+ '\u2764\uFE0F' # HEAVY BLACK HEART + VS16
+ '\u2600\uFE0F' # BLACK SUN WITH RAYS + VS16
+ '\u2615\uFE0F') * 25 # HOT BEVERAGE + VS16
+
+_VS15_TEXT = ('\u2615\uFE0E' # HOT BEVERAGE + VS15 (wide=2 narrows to 1)
+ '\u231A\uFE0E' # WATCH + VS15 (wide=2 narrows to 1)
+ '\u23F0\uFE0E') * 34 # ALARM CLOCK + VS15 (wide=2 narrows to 1)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_vs16_term_program(benchmark, term_program):
+ """Benchmark wcswidth() with VS16 sequences to exercise vs16_narrower bisearch."""
+ benchmark(wcwidth.wcstwidth, _VS16_TEXT, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_wcstwidth_vs15_term_program(benchmark, term_program):
+ """Benchmark wcswidth() with VS15 sequences to exercise vs15_wider bisearch."""
+ benchmark(wcwidth.wcstwidth, _VS15_TEXT, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_width_vs16_term_program(benchmark, term_program):
+ """Benchmark width() with VS16 sequences to exercise vs16_narrower bisearch."""
+ benchmark(wcwidth.width, _VS16_TEXT, term_program=term_program)
+
+
[email protected]('term_program', _TERM_PROGRAMS)
+def test_width_vs15_term_program(benchmark, term_program):
+ """Benchmark width() with VS15 sequences to exercise vs15_wider bisearch."""
+ benchmark(wcwidth.width, _VS15_TEXT, term_program=term_program)
diff --git a/contrib/python/wcwidth/py3/tests/test_clip.py b/contrib/python/wcwidth/py3/tests/test_clip.py
index 8ab3f1d24e8..dee5b8c11cb 100644
--- a/contrib/python/wcwidth/py3/tests/test_clip.py
+++ b/contrib/python/wcwidth/py3/tests/test_clip.py
@@ -389,7 +389,7 @@ def test_clip_tab_first_visible_with_sgr():
def test_clip_overtyping_override_by_control_codes_ignore():
"""When overtyping=True and control_codes='ignore', overtyping is overridden to False."""
- # elif entered: overtyping=True + control_codes='ignore' β†’ overtyping=False
+ # elif entered: overtyping=True + control_codes='ignore': overtyping becomes False
assert clip('hello world', 0, 5, overtyping=True, control_codes='ignore') == 'hello'
# Verify that overtyping is actually disabled: cursor movement chars are
# treated as zero-width, so the result is the same as without overtyping.
@@ -398,7 +398,7 @@ def test_clip_overtyping_override_by_control_codes_ignore():
def test_clip_overtyping_without_ignore():
"""When overtyping=True and control_codes='parse', elif is not entered."""
- # elif skipped: overtyping=True + control_codes='parse' β†’ overtyping stays True
+ # elif skipped: overtyping=True + control_codes='parse': overtyping stays True
# The painter path is used, cursor movement sequences affect output.
assert clip('ab\x1b[2Dcd', 0, 4, overtyping=True, control_codes='parse') == 'cd'
diff --git a/contrib/python/wcwidth/py3/tests/test_core.py b/contrib/python/wcwidth/py3/tests/test_core.py
index dd1e3b7d1b7..9d5007571b1 100644
--- a/contrib/python/wcwidth/py3/tests/test_core.py
+++ b/contrib/python/wcwidth/py3/tests/test_core.py
@@ -199,9 +199,9 @@ def test_balinese_script():
phrase = ("\u1B13" # Category 'Lo', EAW 'N' -- BALINESE LETTER KA
"\u1B28" # Category 'Lo', EAW 'N' -- BALINESE LETTER PA KAPAL
"\u1B2E" # Category 'Lo', EAW 'N' -- BALINESE LETTER LA
- "\u1B44") # Category 'Mc', EAW 'N' -- BALINESE ADEG ADEG
+ "\u1B44") # Category 'Mc', EAW 'N' -- BALINESE ADEG ADEG (virama)
expect_length_each = (1, 1, 1, 0)
- expect_length_phrase = 4
+ expect_length_phrase = 3
# exercise,
length_each = tuple(map(wcwidth.wcwidth, phrase))
@@ -308,7 +308,7 @@ def test_devanagari_script():
"\u093F") # MatraL, Category 'Mc', East Asian Width property 'N' -- DEVANAGARI VOWEL SIGN I
# 23107-terminal-suppt.pdf suggests wcwidth.wcwidth should return (2, 0, 0, 1)
expect_length_each = (1, 0, 1, 0)
- # virama conjunct collapses KA+virama+SSA into one cell, Mc adds +1
+ # grapheme cluster capped at 2 cells (ghostty, foot, Windows Terminal)
expect_length_phrase = 2
# exercise,
@@ -330,7 +330,7 @@ def test_tamil_script():
# 23107-terminal-suppt.pdf suggests wcwidth.wcwidth should return (3, 0, 0, 4)
expect_length_each = (1, 0, 1, 0)
- # virama conjunct collapses KA+virama+SSA into one cell, Mc adds +1
+ # grapheme cluster capped at 2 cells (ghostty, foot, Windows Terminal)
expect_length_phrase = 2
# exercise,
@@ -353,7 +353,7 @@ def test_kannada_script():
"\u0cc8") # MatraUR, Category 'Mc', East Asian Width property 'N' -- KANNADA VOWEL SIGN AI
# 23107-terminal-suppt.pdf suggests should be (2, 0, 3, 1)
expect_length_each = (1, 0, 1, 0)
- # virama conjunct collapses RA+virama+JHA into one cell, Mc adds +1
+ # grapheme cluster capped at 2 cells (ghostty, foot, Windows Terminal)
expect_length_phrase = 2
# exercise,
@@ -454,11 +454,55 @@ def test_virama_conjunct(phrase, expected):
assert wcwidth.width(phrase) == expected
[email protected]("phrase,expected", [
+ ("\u0995\u09CD\u09A4\u09BF", 2), # Bengali C+V+C+Mc: ক্ঀি (capped at 2)
+ ("\u0915\u094D\u0924\u093F", 2), # Devanagari C+V+C+Mc: ΰ€•ΰ₯ΰ€€ΰ€Ώ (capped at 2)
+ ("\u0995\u09CD\u09A4", 2), # C+V+C (no Mc), unchanged
+ ("\u0995\u09BF", 2), # C+Mc (no virama), unchanged
+])
+def test_virama_conjunct_mc_vowel(phrase, expected):
+ """Mc combines into base; cluster capped at 2."""
+ assert wcwidth.wcswidth(phrase) == expected
+ assert wcwidth.width(phrase) == expected
+ assert wcwidth.wcswidth(phrase) == expected
+ assert wcwidth.width(phrase) == expected
+
+
[email protected]("phrase,expected", [
+ ("\uA9A0\uA9C0\uA9B1\uA9C0\uA9AE", 2), # Javanese C+V+C+V+C: TA+PANGKON+SA+PANGKON+WA
+ ("\uA9A0\uA9C0\uA9B1", 2), # Javanese C+V+C: TA+PANGKON+SA
+ ("\u1B04\u1B44\u1B05", 2), # Balinese C+V+C: A+ADEG ADEG+I
+ ("\U000111C0\U000111C0", 0), # Sharada virama alone (zero-width)
+])
+def test_virama_mc_category_overlap(phrase, expected):
+ """Virama codepoints in Mc category check ISC before Mc."""
+ assert wcwidth.wcswidth(phrase) == expected
+ assert wcwidth.width(phrase) == expected
+
+
[email protected]("phrase,expected", [
+ ("\u1000\u1039\u1000", 2), # Burmese KA+VIRAMA+KA
+ ("\u1000\u1039\u1000\u1039\u1002", 2), # Burmese KA+V+KA+V+GA (capped)
+ ("\u1000\u1039\u200D\u1000", 2), # Burmese KA+V+ZWJ+KA
+ ("\u1782\u17D2\u1782\u17C1", 2), # Khmer KO+COENG+KO+VOWEL_E (capped)
+ ("\u1780\u17D2\u1780", 2), # Khmer KA+COENG+KA
+])
+def test_virama_conjunct_invisible_stacker(phrase, expected):
+ assert wcwidth.wcswidth(phrase) == expected
+ assert wcwidth.width(phrase) == expected
+
+
def test_zwj_at_end_of_string():
"""ZWJ at end of string (not after virama) is consumed with zero width."""
assert wcwidth.wcswidth('a\u200D') == 1
+def test_wcswidth_n_exceeds_length():
+ """Verify wcswidth() with n > len(string) does not raise IndexError."""
+ assert wcwidth.wcswidth('hello', n=999) == 5
+ assert wcwidth.wcswidth('\u30B3\u30F3', n=999) == 4
+
+
def test_soft_hyphen():
# Test SOFT HYPHEN, category 'Cf' usually are zero-width, but most
# implementations agree to draw it was '1' cell, visually
@@ -493,12 +537,11 @@ def test_prepended_concatenation_mark_width(codepoint, name):
def test_legacy_module():
"""Verify legacy ``wcwidth.wcwidth`` module's public items are importable."""
# pylint: disable=import-outside-toplevel
- # std imports
- import sys
-
- # Access the legacy submodule via sys.modules (matching 0.6.0 where
- # 'import wcwidth.wcwidth' returned the function, not the module).
- _legacy = sys.modules['wcwidth.wcwidth']
+ # Save and restore wcwidth.wcwidth because importing the submodule
+ # rebinds the package attribute from the function to the module.
+ _wcwidth_func = wcwidth.wcwidth
+ _legacy = __import__('wcwidth.wcwidth', fromlist=['wcwidth'])
+ wcwidth.wcwidth = _wcwidth_func
for name in _legacy.__all__:
attr = getattr(_legacy, name)
diff --git a/contrib/python/wcwidth/py3/tests/test_term_overrides.py b/contrib/python/wcwidth/py3/tests/test_term_overrides.py
new file mode 100644
index 00000000000..0a7162ef800
--- /dev/null
+++ b/contrib/python/wcwidth/py3/tests/test_term_overrides.py
@@ -0,0 +1,597 @@
+"""Tests for terminal-specific width overrides."""
+# std imports
+import os
+
+# 3rd party
+import pytest
+
+# local
+import wcwidth
+import wcwidth.table_grapheme_overrides as grapheme_overrides
+from wcwidth._constants import (_EMPTY_OVERRIDES,
+ _merge_ranges,
+ resolve_terminal,
+ get_term_overrides,
+ list_term_programs)
+from wcwidth.table_overrides import VS15_OVERRIDES
+
+
+def test_resolve_terminal_aliases():
+ """resolve_terminal maps known aliases to canonical names."""
+ assert resolve_terminal('kitty') == 'kitty'
+ assert resolve_terminal('vscode') == 'xterm.js'
+ assert resolve_terminal('urxvt') == 'urxvt'
+
+
+def test_resolve_terminal_unknown():
+ """resolve_terminal returns None for unrecognized names and empty string."""
+ assert resolve_terminal('nonexistent') is None
+ assert resolve_terminal('') is None
+
+
+def test_resolve_terminal_auto_detect():
+ """resolve_terminal=True reads TERM_PROGRAM env var, falling back to TERM."""
+ saved_tprog = os.environ.get('TERM_PROGRAM')
+ saved_term = os.environ.get('TERM')
+ try:
+ for var in ('TERM_PROGRAM', 'TERM'):
+ os.environ.pop(var, None)
+ resolve_terminal.cache_clear()
+ assert resolve_terminal(True) is None
+ os.environ['TERM_PROGRAM'] = 'kitty'
+ resolve_terminal.cache_clear()
+ assert resolve_terminal(True) == 'kitty'
+ finally:
+ for var, saved in (('TERM_PROGRAM', saved_tprog), ('TERM', saved_term)):
+ if saved is not None:
+ os.environ[var] = saved
+ else:
+ os.environ.pop(var, None)
+ resolve_terminal.cache_clear()
+
+
+def test_wcswidth_no_override():
+ """Wcswidth works normally without term_program or with empty string."""
+ assert wcwidth.wcswidth('hello') == 5
+ assert wcwidth.wcstwidth('hello', term_program='') == 5
+
+
[email protected]('char,expected_default,expected_vte', [
+ ('\u2630', 2, 1),
+ ('\U0001f1e6', 2, 1),
+])
+def test_wcswidth_vte_override(char, expected_default, expected_vte):
+ """VTE override narrows wide characters."""
+ assert wcwidth.wcswidth(char) == expected_default
+ assert wcwidth.wcstwidth(char, term_program='VTE') == expected_vte
+
+
[email protected]('text,kwargs,expected', [
+ ('\u2630', {'term_program': 'VTE'}, 1),
+ ('\u2630', {'term_program': 'kitty'}, 2),
+ ('\x1b[31m\u2630\u2631\x1b[0m', {'term_program': 'VTE'}, 2),
+ ('\u2630\u2631', {'control_codes': 'ignore', 'term_program': 'VTE'}, 2),
+])
+def test_width_vte_override(text, kwargs, expected):
+ """Width() applies VTE overrides with and without control codes."""
+ assert wcwidth.width(text, **kwargs) == expected
+
+
+def test_vs16_override_basic():
+ """VS16 override is applied to heart emoji variation."""
+ heart_vs16 = '\u2764\ufe0f'
+ assert wcwidth.wcswidth(heart_vs16) == 2
+ assert wcwidth.wcstwidth(heart_vs16, term_program='VTE') == 1
+ assert wcwidth.width(heart_vs16, term_program='VTE') == 1
+
+
+def test_vs16_libvterm_no_override():
+ """Libvterm is not a known terminal; falls back to spec VS16 (returns 2)."""
+ assert wcwidth.wcstwidth('\u23ed\ufe0f', term_program='libvterm') == 2
+ assert wcwidth.width('\u23ed\ufe0f', term_program='libvterm') == 2
+
+
+def test_wcwidth_unchanged():
+ """Wcwidth() does not accept term_program parameter."""
+ assert wcwidth.wcwidth('\u2630') == 2
+ with pytest.raises(TypeError):
+ wcwidth.wcwidth('\u2630', term_program='VTE') # type: ignore[call-arg]
+
+
+def test_wcstwidth_term_program():
+ """Empty term_program disables override lookup."""
+ assert wcwidth.wcstwidth('\u2630', term_program='') == 2
+ assert wcwidth.wcstwidth('\u2630', term_program='VTE') == 1
+ assert wcwidth.wcswidth('\u2630') == 2
+ assert wcwidth.width('\u2630') == 2
+
+
+def test_wcswidth_ascii_unchanged():
+ """ASCII text is unaffected by terminal overrides."""
+ assert wcwidth.wcstwidth('hello world', term_program='VTE') == 11
+ assert wcwidth.wcstwidth('hello world', term_program='kitty') == 11
+
+
+def test_vs15_standalone():
+ """VS15 (U+FE0E) alone measures as width 0."""
+ assert wcwidth.wcswidth('\ufe0e') == 0
+ assert wcwidth.wcstwidth('\ufe0e', term_program='VTE') == 0
+
+
+def test_vs15_no_override():
+ """VS15 after a character not in any override table has no effect."""
+ base = '\u2630'
+ assert wcwidth.wcswidth(base + '\ufe0e') == wcwidth.wcswidth(base)
+ assert wcwidth.wcstwidth(base + '\ufe0e', term_program='kitty') == wcwidth.wcstwidth(base)
+
+
+def test_vs15_wider_override_unchanged():
+ """VS15 narrows by default; VTE wider override restores width 2."""
+ assert wcwidth.wcswidth('\u231a') == 2
+ assert wcwidth.wcswidth('\u231a\ufe0e') == 1
+ assert wcwidth.wcstwidth('\u231a\ufe0e', term_program='VTE') == 2
+ assert wcwidth.width('\u231a\ufe0e') == 1
+ assert wcwidth.width('\u231a\ufe0e', term_program='VTE') == 2
+
+
+def test_grapheme_override_zwj_not_in_table():
+ """ZWJ cluster not in override table falls through without error."""
+ assert wcwidth.wcstwidth('πŸ˜€\u200dπŸ˜€', term_program='VTE') == 2
+ assert wcwidth.width('πŸ˜€\u200dπŸ˜€', term_program='VTE') == 2
+
+
+def test_width_vs16_zwj_transition():
+ """Width() VS16-applied state transitions to ZWJ_BLOCKED."""
+ # smiley gets VS16'd (base_state=VS16_APPLIED), then ZWJ_BLOCKED
+ assert wcwidth.width('\u263a\ufe0f\u200da') == 2
+
+
+def test_width_vs15_override():
+ """Width() with VS15 and terminal override."""
+ assert wcwidth.width('\u231a\ufe0e', term_program='VTE') == 2
+ assert wcwidth.width('\u2630\ufe0e', term_program='VTE') == 1
+
+
[email protected]('term_program,expected', [
+ (False, 2),
+ ('', 2),
+ ('nonexistent', 2),
+ ('alacritty', 4),
+])
+def test_grapheme_override_wcswidth_family(term_program, expected):
+ """Wcswidth ZWJ grapheme override applied only for recognized terminals with overrides."""
+ family = '\U0001F468\u200D\U0001F466'
+ assert wcwidth.wcstwidth(family, term_program=term_program) == expected
+
+
+def test_grapheme_override_multi_zwj_alacritty():
+ """Wcswidth handles multi-ZWJ grapheme override."""
+ family4 = '\U0001F468\u200D\U0001F469\u200D\U0001F467\u200D\U0001F466'
+ default = wcwidth.wcswidth(family4)
+ override = wcwidth.wcstwidth(family4, term_program='alacritty')
+ assert default == 2
+ assert override == 8
+
+
[email protected]('func,kwargs', [
+ (wcwidth.width, {'term_program': 'alacritty'}),
+ (wcwidth.width, {'control_codes': 'ignore', 'term_program': 'alacritty'}),
+])
+def test_grapheme_override_width_alacritty(func, kwargs):
+ """Width() applies ZWJ grapheme override for alacritty."""
+ family = '\U0001F468\u200D\U0001F466'
+ assert func(family, **kwargs) == 4
+
+
+def test_grapheme_override_ascii_unchanged():
+ """ASCII text is unaffected by grapheme overrides."""
+ assert wcwidth.wcstwidth('hello', term_program='alacritty') == 5
+ assert wcwidth.width('hello', term_program='alacritty') == 5
+
+
+def test_grapheme_override_zwj_at_end():
+ """ZWJ at end of string does not trigger override scan."""
+ text = '\U0001F468\u200D'
+ assert wcwidth.wcstwidth(text, term_program='alacritty') == 2
+
+
+def test_grapheme_override_fitzpatrick():
+ """Fitzpatrick modifier between base and ZWJ handled correctly."""
+ text = '\u26F9\U0001F3FB\u200D\u2640\uFE0F'
+ assert wcwidth.wcstwidth(text, term_program='alacritty') == 4
+
+
+def test_list_term_programs():
+ """list_term_programs returns known terminals."""
+ terms = list_term_programs()
+ assert isinstance(terms, tuple)
+ assert 'alacritty' in terms
+ assert 'vte' in terms
+ assert 'xterm.js' in terms
+ assert 'nonexistent' not in terms
+
+
+def test_grapheme_override_invalid_term_names():
+ """Grapheme override get() returns empty dict for invalid names."""
+ assert grapheme_overrides.get(None) == {}
+ assert grapheme_overrides.get('__init__') == {}
+ assert grapheme_overrides.get('') == {}
+ assert grapheme_overrides.get('../../etc') == {}
+
+
+def test_grapheme_override_zwj_no_extpict_base():
+ """ZWJ after non-ExtPict base does not trigger override scan."""
+ text = 'a\u200D\u200D'
+ assert wcwidth.wcstwidth(text, term_program='alacritty') == 1
+
+
[email protected]('text,term,expected', [
+ ('πŸ‘¨\u200dπŸ‘¦x', 'alacritty', 5),
+ ('πŸ‘¨\u200da', 'alacritty', 2),
+ ('πŸ‘¨\u200da', False, 2),
+])
+def test_grapheme_override_scanner_edges(text, term, expected):
+ """Scanner edge cases for ZWJ chains."""
+ assert wcwidth.wcstwidth(text, term_program=term) == expected
+
+
+def test_grapheme_override_missing_module():
+ """
+ Returns None when registry hash points to missing _known_ module.
+
+ This can occur during a program re-install when the registry and _known_* files are out of sync
+ (filesystem vs. in-memory copy differ). The ImportError is caught so measurement can continue
+ gracefully without per-terminal grapheme overrides.
+ """
+ saved = grapheme_overrides._REGISTRY.get('putty')
+ try:
+ grapheme_overrides._REGISTRY['putty'] = 'deadbeef'
+ grapheme_overrides.get.cache_clear()
+ assert grapheme_overrides.get('putty') == {}
+ finally:
+ grapheme_overrides._REGISTRY['putty'] = saved
+ grapheme_overrides.get.cache_clear()
+
+
+def test_no_terminal_has_vs15_narrower_overrides():
+ """No terminal narrows VS15."""
+
+ # VS15 (text presentation) narrows a wide character to width 1. There is no width below 1 !
+ narrower_terminals = {
+ term: data['narrower']
+ for term, data in VS15_OVERRIDES.items()
+ if data.get('narrower')
+ }
+ assert not narrower_terminals, (
+ f'Unexpected: terminal(s) with VS15 narrower overrides detected: '
+ f'{sorted(narrower_terminals)}.\n'
+ f'VS15 cannot narrow a character below width 1. '
+ f'This may indicate a ucs-detect measurement error or an unexpected terminal behavior.'
+ )
+
+
+def test_list_term_programs_includes_xterm():
+ """Xterm is a recognized terminal program for explicit use."""
+ assert 'xterm' in list_term_programs()
+
+
+def test_resolve_terminal_xterm_explicit():
+ """resolve_terminal returns 'xterm' when passed explicitly."""
+ assert resolve_terminal('xterm') == 'xterm'
+
+
[email protected]('env_var', ['TERM', 'TERM_PROGRAM'])
+def test_resolve_terminal_xterm_auto_detected(env_var):
+ """resolve_terminal returns 'xterm' for xterm via auto-detection from env."""
+ os.environ[env_var] = 'xterm'
+ resolve_terminal.cache_clear()
+ assert resolve_terminal(True) == 'xterm'
+
+
[email protected]('func,text,expected_default,expected_xterm', [
+ (wcwidth.wcstwidth, '\U0001f1e6', 2, 1),
+ (wcwidth.width, '\U0001f1e6', 2, 1),
+ (wcwidth.wcstwidth, '\u231a\ufe0e', 1, 2),
+ (wcwidth.width, '\u231a\ufe0e', 1, 2),
+])
+def test_xterm_overrides_applied(func, text, expected_default, expected_xterm):
+ """Xterm overrides are applied when term_program='xterm' is explicit."""
+ assert func(text) == expected_default
+ assert func(text, term_program='xterm') == expected_xterm
+
+
[email protected]('func', [wcwidth.wcswidth, wcwidth.width])
+def test_zwj_fallthrough_resets_base_for_vs16(func):
+ """VS16 after ZWJ-skipped char does not connect to stale base (before fix, VS16 narrowed the
+ watch)."""
+ assert func('\u231a\u200d\u23f0\ufe0f') == 2
+
+
[email protected]('func', [wcwidth.wcswidth, wcwidth.width])
+def test_zwj_fallthrough_resets_base_for_vs15(func):
+ """VS15 after ZWJ-skipped char does not connect to stale base (before fix, VS15 narrowed the
+ watch)."""
+ assert func('\u231a\u200d\u23f0\ufe0e') == 2
+
+
[email protected]('func,text,dest_width,expected', [
+ (wcwidth.ljust, '\u2630', 4, '\u2630 '),
+ (wcwidth.rjust, '\u2630', 4, ' \u2630'),
+ (wcwidth.center, '\u2630', 5, ' \u2630 '),
+])
+def test_align_term_program_vte(func, text, dest_width, expected):
+ """Ljust/rjust/center pass term_program through to width()."""
+ assert func(text, dest_width, term_program='VTE') == expected
+
+
+def test_clip_term_program_vte():
+ """Clip() passes term_program through to width()."""
+ result = wcwidth.clip('\u2630\u2631', 0, 1, term_program='VTE')
+ assert result == '\u2630'
+
+
+def test_wrap_term_program_vte():
+ """Wrap() passes term_program through to width()."""
+ result = wcwidth.wrap('\u2630\u2631', width=2, term_program='VTE')
+ assert result == ['\u2630\u2631']
+
+
[email protected]('termenv,expected', [
+ ({'TERM': 'xterm-kitty'}, 'kitty'),
+ ({'TERM_PROGRAM': '', 'TERM': 'xterm-kitty'}, 'kitty'),
+])
+def test_resolve_terminal_from_env(termenv, expected):
+ """resolve_terminal reads TERM when TERM_PROGRAM is unset or empty."""
+ for var in ('TERM_PROGRAM', 'TERM'):
+ os.environ.pop(var, None)
+ os.environ.update(termenv)
+ resolve_terminal.cache_clear()
+ assert resolve_terminal(True) == expected
+
+
[email protected]('args,expected', [
+ ((), ()),
+ ((((1, 5),),), ((1, 5),)),
+ ((((1, 3),), ((6, 8),)), ((1, 3), (6, 8))),
+ ((((1, 5),), ((4, 8),)), ((1, 8),)),
+])
+def test_merge_ranges(args, expected):
+ """_merge_ranges merges sorted range tuples."""
+ assert _merge_ranges(*args) == expected
+
+
+def test_sfz_override_foot():
+ """Foot narrows Fitzpatrick modifiers."""
+ assert wcwidth.wcswidth('\U0001F3FB') == 2
+ assert wcwidth.wcstwidth('\U0001F3FB', term_program='foot') == 1
+
+
[email protected]('term_program,expected', [
+ ('kitty', 0),
+ ('bobcat', 0),
+ (False, 2),
+])
+def test_sfz_zeroer(term_program, expected):
+ """Standalone Fitzpatrick modifiers zeroed per terminal."""
+ assert wcwidth.wcswidth('\U0001F3FB') == 2
+ assert wcwidth.wcstwidth('\U0001F3FB', term_program=term_program) == expected
+
+
[email protected]('kwargs,expected', [
+ ({}, 0),
+ ({'control_codes': 'ignore'}, 0),
+])
+def test_width_zeroer(kwargs, expected):
+ """Width() zeroes standalone Fitzpatrick modifiers for kitty."""
+ assert wcwidth.width('\U0001F3FB', term_program='kitty', **kwargs) == expected
+
+
+def test_empty_overrides_includes_zeroer():
+ """_EMPTY_OVERRIDES has six empty tuple fields."""
+ assert _EMPTY_OVERRIDES.narrower == ()
+ assert _EMPTY_OVERRIDES.vs16_narrower == ()
+ assert _EMPTY_OVERRIDES.vs15_wider == ()
+ assert _EMPTY_OVERRIDES.zeroer == ()
+ assert _EMPTY_OVERRIDES.narrow_wider == ()
+ assert _EMPTY_OVERRIDES.narrow_zeroer == ()
+
+
+def test_get_term_overrides_returns_empty_when_no_overrides():
+ """get_term_overrides returns _EMPTY_OVERRIDES when terminal has no override data."""
+ get_term_overrides.cache_clear()
+ overrides = get_term_overrides('no-such-terminal')
+ assert overrides is _EMPTY_OVERRIDES
+
+
+def test_get_term_overrides_reads_narrow_zeroer_key():
+ """get_term_overrides reads 'narrow_zeroer' key from NARROW_OVERRIDES."""
+ get_term_overrides.cache_clear()
+ overrides = get_term_overrides('kitty')
+ assert len(overrides.narrow_zeroer) == 9
+ assert overrides.narrow_zeroer[0] == (0x00AD, 0x00AD)
+
+
+def test_get_term_overrides_narrow_wider_still_empty():
+ """get_term_overrides narrow_wider is empty when no 'wider' entries exist."""
+ get_term_overrides.cache_clear()
+ overrides = get_term_overrides('konsole')
+ assert overrides.narrow_wider == ()
+
+
[email protected]('func', [wcwidth.wcstwidth, wcwidth.width])
+def test_narrow_wider_width(func):
+ """Verify width() & wcstwidth() match result of narrow_wider overrides."""
+ assert wcwidth.wcswidth('\u261d') == 1
+ assert func('\u261d', term_program='kitty') == 2
+
+
[email protected]('codepoint', [
+ '\u00ad',
+ '\u0600',
+ '\u0605',
+ '\u06dd',
+ '\u070f',
+ '\u0890',
+ '\u0891',
+ '\u08e2',
+ '\U000110bd',
+ '\U000110cd',
+])
+def test_narrow_zeroer_cf_codepoints(codepoint):
+ """Cf format characters are zeroed by kitty/konsole/wezterm narrow_zeroer."""
+ assert wcwidth.wcswidth(codepoint) == 1
+ assert wcwidth.wcstwidth(codepoint, term_program='kitty') == 0
+ assert wcwidth.wcstwidth(codepoint, term_program='konsole') == 0
+ assert wcwidth.wcstwidth(codepoint, term_program='wezterm') == 0
+
+
[email protected]('func', [wcwidth.wcstwidth, wcwidth.width])
+def test_narrow_zeroer_not_applied_for_other_terminals(func):
+ """Terminals without narrow overrides keep width 1 for Cf characters."""
+ assert func('\u0600', term_program='xterm') == 1
+ assert func('\u0600', term_program='ghostty') == 1
+ assert func('\u0600', term_program='') == 1
+
+
[email protected]('func,term_program,expected', [
+ (wcwidth.wcstwidth, 'kitty', 0),
+ (wcwidth.width, 'kitty', 0),
+ (wcwidth.wcstwidth, 'konsole', 0),
+ (wcwidth.width, 'konsole', 0),
+ (wcwidth.wcstwidth, 'wezterm', 0),
+ (wcwidth.width, 'wezterm', 0),
+])
+def test_narrow_zeroer_width(func, term_program, expected):
+ """Width() matches wcstwidth() for narrow_zeroer overrides."""
+ assert func('\u0600', term_program=term_program) == expected
+
+
[email protected]('value,expected', [
+ (' KITTY ', 'kitty'),
+ (' ', None),
+])
+def test_resolve_terminal_strips_whitespace(value, expected):
+ """resolve_terminal strips, lowercases, and returns None for whitespace-only."""
+ assert resolve_terminal(value) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u1000\u1031', False, 2), # MYANMAR LETTER KA + MYANMAR VOWEL SIGN E (Burmese)
+ ('\u1000\u1031', '', 2),
+ ('\u1000\u1031', 'kitty', 1),
+ ('\u1000\u1031', 'foot', 1),
+ ('\u1000\u1031', 'alacritty', 2),
+ ('\u0c05\u0c02', False, 2), # TELUGU LETTER A + TELUGU SIGN ANUSVARA
+ ('\u0c05\u0c02', 'kitty', 1),
+ ('\u0e01\u0e33', False, 2), # THAI CHARACTER KO KAI + THAI CHARACTER SARA AM
+ ('\u0e01\u0e33', 'kitty', 1),
+ ('\u0985\u0982', False, 2), # BENGALI LETTER A + BENGALI SIGN ANUSVARA
+ ('\u0985\u0982', 'kitty', 1),
+ ('\u0915\u093e', False, 2), # DEVANAGARI LETTER KA + DEVANAGARI VOWEL SIGN AA
+ ('\u0915\u093e', 'kitty', 1),
+ ('\u0915\u093e', 'foot', 1),
+ ('\u0915\u093e', 'alacritty', 2),
+])
+def test_wcswidth_language_grapheme(text, term_program, expected):
+ """Language grapheme clusters use per-terminal override tables."""
+ assert wcwidth.wcstwidth(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u1000\u1031', 'kitty', 1), # MYANMAR LETTER KA + VOWEL SIGN E
+ ('\u1000\u1031', 'foot', 1),
+ ('\u0915\u093e', 'kitty', 1), # DEVANAGARI LETTER KA + VOWEL SIGN AA
+ ('\u0915\u093e', 'foot', 1),
+ ('\u0c05\u0c02', 'kitty', 1), # TELUGU LETTER A + SIGN ANUSVARA
+])
+def test_width_language_grapheme(text, term_program, expected):
+ """Width() applies language grapheme overrides."""
+ assert wcwidth.width(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('hello', 'kitty', 5),
+ ('hello', 'foot', 5),
+ ('hello\u1000\u1031', 'kitty', 6), # ASCII + Burmese KA + VOWEL SIGN E
+ ('\u1000\u1031hello', 'kitty', 6), # Burmese KA + VOWEL SIGN E + ASCII
+ ('\u1000\u1031\u1000\u1031', 'kitty', 2), # two Burmese KA+E clusters
+ ('\u1000\u1031x\u0915\u093e', 'kitty', 3), # Burmese + ASCII + Devanagari
+])
+def test_wcswidth_mixed_language_ascii(text, term_program, expected):
+ """Language grapheme overrides do not affect ASCII and mix correctly."""
+ assert wcwidth.wcstwidth(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u1000\u1039\u1001', False, 2), # MYANMAR KA + VIRAMA + KHA (conjunct)
+ ('\u1000\u1039\u1001', 'kitty', 1),
+ ('\u1000\u1039\u1001', 'foot', 2),
+ ('\u1000\u103b\u102d\u102f', False, 2), # MYANMAR KA + MEDIAL YA + VOWEL I + VOWEL U
+ ('\u1000\u103b\u102d\u102f', 'kitty', 1),
+ ('\u1000\u103b\u102d\u102f', 'foot', 1),
+])
+def test_wcswidth_virama_conjunct(text, term_program, expected):
+ """Virama conjunct grapheme clusters use per-terminal overrides."""
+ assert wcwidth.wcstwidth(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u0915\u094D\u200D\u0937', False, 2), # Devanagari C+Virama+ZWJ+C (explicit ZWJ conjunct)
+ ('\u0915\u094D\u200D\u0937', 'xterm', 2),
+])
+def test_wcswidth_virama_zwj_conjunct(text, term_program, expected):
+ """Virama+ZWJ conjunct skips ZWJ and forms a capped conjunct."""
+ assert wcwidth.wcstwidth(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u1000\u1031', 'xterm', 2), # Burmese: no xterm override
+ ('\u0915\u093e', 'xterm', 2), # Devanagari: no xterm override
+ ('\u0c05\u0c02', 'xterm', 2), # Telugu: no xterm override
+])
+def test_wcswidth_language_no_override(text, term_program, expected):
+ """Terminals without language overrides return spec width."""
+ assert wcwidth.wcstwidth(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u1000\u1031X', 'kitty', 2), # Burmese C+Mc mid-string flush via cluster_text
+ ('X\u1000\u1031', 'kitty', 2), # ASCII then Burmese C+Mc append
+ ('\u1000\u1031\u1000\u1031', 'kitty', 2), # two Burmese C+Mc with flush between
+ ('\u0915\u093eX', 'kitty', 2), # Devanagari C+Mc mid-string flush
+])
+def test_width_cluster_text_override_mid_string(text, term_program, expected):
+ """Width() flushes cluster via cluster_text override when followed by another char."""
+ assert wcwidth.width(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\u0e01\u0e33X', 'kitty', 2), # Thai C+Lo override pair then ASCII
+ ('X\u0e01\u0e33', 'kitty', 2), # ASCII then Thai override pair
+ ('\u0e01\u0e33\u0e01\u0e33', 'kitty', 2), # two Thai override pairs
+])
+def test_width_candidate_override_mid_string(text, term_program, expected):
+ """Width() flushes cluster via candidate override when two Lo chars form a pair."""
+ assert wcwidth.width(text, term_program=term_program) == expected
+
+
[email protected]('text,term_program,expected', [
+ ('\r\u1000\u1031', 'xterm', 2), # CR reset, cluster extends beyond prior max_extent
+ ('\r\u0915\u093e', 'xterm', 2), # Devanagari same pattern
+ ('XX\b\b\u1000\u1031', 'xterm', 2), # backspace then cluster does not exceed prior max
+])
+def test_width_epilogue_max_extent_update(text, term_program, expected):
+ """Width() updates max_extent in epilogue when final cluster extends beyond prior max."""
+ assert wcwidth.width(text, term_program=term_program) == expected
+
+
+def test_wcstwidth_ambiguous_width_2():
+ """Wcstwidth respects ambiguous_width parameter."""
+ assert wcwidth.wcstwidth('\u00b1', ambiguous_width=2, term_program='VTE') == 2
+ assert wcwidth.wcstwidth('\u00b1', ambiguous_width=2, term_program='') == 2
+
+
+def test_wcstwidth_control_character():
+ """Wcstwidth returns -1 for C0 control characters."""
+ assert wcwidth.wcstwidth('hello\x01world', term_program='VTE') == -1
+ assert wcwidth.wcstwidth('\x01', term_program='') == -1
diff --git a/contrib/python/wcwidth/py3/tests/test_text_sizing.py b/contrib/python/wcwidth/py3/tests/test_text_sizing.py
index b5e18085e35..1566c3192d4 100644
--- a/contrib/python/wcwidth/py3/tests/test_text_sizing.py
+++ b/contrib/python/wcwidth/py3/tests/test_text_sizing.py
@@ -155,6 +155,7 @@ def test_text_sizing_sequence(given_sequence, expected_text, expected_params, ex
('\x1b]66;w=2;A\x07\x1b]66;w=3;B\x07', 5),
('\x1b]66;s=2:w=3;text\x1b\\', 6),
('\x1b[31m\x1b]66;w=2;AB\x07\x1b[0m', 2),
+ ('\x1b]66;;\x01\x07', 0),
])
def test_strings_with_text_sizing(text, expected):
"""Verify measured width strings containing OSC66."""
@@ -294,7 +295,7 @@ def test_clip_text_sizing_scaled_with_fillchar(text, start, end, expected):
def test_clip_simple_path_padding():
"""Simple-path clip with w=N larger than text length exercises padding loop."""
- # w=4 but only 1 grapheme 'X' β€” 3 empty units are padded.
+ # w=4 but only 1 grapheme 'X': 3 empty units are padded.
# Clip window (0, 1) forces partial overlap, triggering
# _text_sizing_clip_simple's padding branch.
assert repr(clip('\x1b]66;w=4;X\x07', 0, 1)) == repr('\x1b]66;w=1;X\x07')
diff --git a/contrib/python/wcwidth/py3/tests/test_width.py b/contrib/python/wcwidth/py3/tests/test_width.py
index 8e43b47b1de..436ca5caf6e 100644
--- a/contrib/python/wcwidth/py3/tests/test_width.py
+++ b/contrib/python/wcwidth/py3/tests/test_width.py
@@ -170,6 +170,12 @@ EDGE_CASES = [
('\x1b', 0, 'lone_ESC'),
('\x1b!', 1, 'ESC_unrecognized'),
('*\x1b*', 2, 'lone_ESC_between_text'),
+ ('\tA\u0CBE\rB\u0CBE\b', 10, 'backspace_flush_below_max_extent'),
+ ('\tA\u0CBE\rB\u0CBE\x1b', 10, 'esc_flush_below_max_extent'),
+ ('\tA\u0CBE\rB\u0CBE\x00', 10, 'zero_width_flush_below_max_extent'),
+ ('\tA\u0CBE\rB\u0CBE\x01', 10, 'illegal_ctrl_flush_below_max_extent'),
+ ('\tA\u0CBE\rB\u0CBE\n', 10, 'vertical_ctrl_flush_below_max_extent'),
+ ('\u4e2d\u0bcd\u4e2d\u0bcd\u4e2d\u0bcd\u4e2d', 2, 'glitch_virama_chain_capped'),
]