summaryrefslogtreecommitdiffstats
path: root/contrib/python/wcwidth/py3/tests
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-02-11 20:50:19 +0300
committerrobot-piglet <[email protected]>2026-02-11 21:10:56 +0300
commitc377d73a156c964ac8c78b11a1e75fa667d26da5 (patch)
treed61be80413be895e7a4e2d2057004982b2f7b0c7 /contrib/python/wcwidth/py3/tests
parent09047cebd54187751addb4fa71a9d3a9fa0f19b1 (diff)
Intermediate changes
commit_hash:260a33021d6afd633fd9e204567374069239f62a
Diffstat (limited to 'contrib/python/wcwidth/py3/tests')
-rw-r--r--contrib/python/wcwidth/py3/tests/test_benchmarks.py59
-rw-r--r--contrib/python/wcwidth/py3/tests/test_clip.py45
-rw-r--r--contrib/python/wcwidth/py3/tests/test_core.py14
-rw-r--r--contrib/python/wcwidth/py3/tests/test_emojis.py25
-rw-r--r--contrib/python/wcwidth/py3/tests/test_sgr_state.py238
-rw-r--r--contrib/python/wcwidth/py3/tests/test_textwrap.py138
-rw-r--r--contrib/python/wcwidth/py3/tests/test_ucslevel.py207
7 files changed, 508 insertions, 218 deletions
diff --git a/contrib/python/wcwidth/py3/tests/test_benchmarks.py b/contrib/python/wcwidth/py3/tests/test_benchmarks.py
index 6e0ffadcc45..9506d9887c2 100644
--- a/contrib/python/wcwidth/py3/tests/test_benchmarks.py
+++ b/contrib/python/wcwidth/py3/tests/test_benchmarks.py
@@ -198,6 +198,41 @@ def test_wrap_with_ansi(benchmark):
benchmark(wcwidth.wrap, text, 20)
+def test_wrap_with_ansi_no_propagate(benchmark):
+ """Benchmark wrap() with ANSI but SGR propagation disabled."""
+ text = '\x1b[31mThe quick brown fox\x1b[0m jumps over the lazy dog'
+ benchmark(wcwidth.wrap, text, 20, propagate_sgr=False)
+
+
+def test_wrap_complex_sgr(benchmark):
+ """Benchmark wrap() with complex SGR (256-color, multiple attributes)."""
+ text = '\x1b[1;3;38;5;208mBold italic orange text that wraps\x1b[0m'
+ benchmark(wcwidth.wrap, text, 10)
+
+
+def test_wrap_hyperlink_no_id(benchmark):
+ """Benchmark wrap() with OSC 8 hyperlinks without id (requires id generation)."""
+ link = '\x1b]8;;https://example.com/path\x1b\\click here for details\x1b]8;;\x1b\\'
+ text = f'See {link} and also {link} for more. Read {link} now. ' * 10
+ benchmark(wcwidth.wrap, text, 40)
+
+
+def test_wrap_hyperlink_with_id(benchmark):
+ """Benchmark wrap() with OSC 8 hyperlinks with existing ids."""
+ link1 = '\x1b]8;id=a;https://example.com\x1b\\click here for details\x1b]8;;\x1b\\'
+ link2 = '\x1b]8;id=b;https://other.org\x1b\\visit this page now\x1b]8;;\x1b\\'
+ text = f'See {link1} and also {link2} for more. Read {link1} now. ' * 10
+ benchmark(wcwidth.wrap, text, 40)
+
+
+def test_wrap_hyperlink_mixed(benchmark):
+ """Benchmark wrap() with mixed OSC 8 hyperlinks (with and without ids)."""
+ no_id = '\x1b]8;;https://example.com\x1b\\link without id here\x1b]8;;\x1b\\'
+ with_id = '\x1b]8;id=x;https://other.org\x1b\\link with id here\x1b]8;;\x1b\\'
+ text = f'First {no_id} then {with_id} and {no_id} again {with_id} end. ' * 10
+ benchmark(wcwidth.wrap, text, 40)
+
+
def test_clip_ascii(benchmark):
"""Benchmark clip() with ASCII string."""
benchmark(wcwidth.clip, 'hello world', 0, 5)
@@ -214,6 +249,30 @@ def test_clip_with_ansi(benchmark):
benchmark(wcwidth.clip, text, 0, 3)
+def test_clip_with_ansi_no_propagate(benchmark):
+ """Benchmark clip() with ANSI but SGR propagation disabled."""
+ text = '\x1b[31m中文字\x1b[0m'
+ benchmark(wcwidth.clip, text, 0, 3, propagate_sgr=False)
+
+
+def test_clip_complex_sgr(benchmark):
+ """Benchmark clip() with complex SGR clipping from middle."""
+ text = '\x1b[1;38;5;208mHello world text\x1b[0m'
+ benchmark(wcwidth.clip, text, 6, 11)
+
+
+def test_propagate_sgr_multiline(benchmark):
+ """Benchmark propagate_sgr() with multiple lines."""
+ lines = ['\x1b[1;31mline one', 'line two', 'line three\x1b[0m']
+ benchmark(wcwidth.propagate_sgr, lines)
+
+
+def test_propagate_sgr_no_sequences(benchmark):
+ """Benchmark propagate_sgr() fast path (no sequences)."""
+ lines = ['line one', 'line two', 'line three']
+ benchmark(wcwidth.propagate_sgr, lines)
+
+
def test_strip_sequences_simple(benchmark):
"""Benchmark strip_sequences() with simple ANSI codes."""
text = '\x1b[31mred\x1b[0m'
diff --git a/contrib/python/wcwidth/py3/tests/test_clip.py b/contrib/python/wcwidth/py3/tests/test_clip.py
index 8a98c14c6f2..995d383a8ac 100644
--- a/contrib/python/wcwidth/py3/tests/test_clip.py
+++ b/contrib/python/wcwidth/py3/tests/test_clip.py
@@ -111,15 +111,24 @@ def test_clip_sequences_before_start():
def test_clip_sequences_after_end():
- assert clip('hello\x1b[31m world\x1b[0m', 0, 5) == 'hello\x1b[31m\x1b[0m'
+ # With propagate_sgr=True (default), no style active at start, so no prefix
+ assert clip('hello\x1b[31m world\x1b[0m', 0, 5) == 'hello'
+ # With propagate_sgr=False, all sequences preserved
+ assert clip('hello\x1b[31m world\x1b[0m', 0, 5, propagate_sgr=False) == 'hello\x1b[31m\x1b[0m'
def test_clip_sequences_multiple():
- assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4) == '\x1b[1m\x1b[31mbold\x1b[0m'
+ # With propagate_sgr=True (default), sequences collapsed to minimal
+ assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4) == '\x1b[1;31mbold\x1b[0m'
+ # With propagate_sgr=False, all sequences preserved separately
+ assert clip('\x1b[1m\x1b[31mbold red\x1b[0m', 0, 4, propagate_sgr=False) == '\x1b[1m\x1b[31mbold\x1b[0m'
def test_clip_sequences_only():
- assert clip('\x1b[31m\x1b[0m', 0, 10) == '\x1b[31m\x1b[0m'
+ # With propagate_sgr=True (default), no visible text means empty result
+ assert clip('\x1b[31m\x1b[0m', 0, 10) == ''
+ # With propagate_sgr=False, sequences preserved
+ assert clip('\x1b[31m\x1b[0m', 0, 10, propagate_sgr=False) == '\x1b[31m\x1b[0m'
def test_clip_sequences_osc_hyperlink():
@@ -131,6 +140,10 @@ def test_clip_sequences_cjk_with_sequences():
assert clip('\x1b[31m中文\x1b[0m', 0, 3) == '\x1b[31m中 \x1b[0m'
+def test_clip_sequences_partial_wide_at_start():
+ assert clip('\x1b[31m中文\x1b[0m', 1, 4) == '\x1b[31m 文\x1b[0m'
+
+
def test_clip_sequences_between_chars():
assert clip('a\x1b[31mb\x1b[0mc', 1, 2) == '\x1b[31mb\x1b[0m'
@@ -167,6 +180,27 @@ def test_clip_combining_multiple():
assert clip('e\u0301\u0327', 0, 1) == 'e\u0301\u0327'
+def test_clip_zero_width_position_bounds():
+ # Standalone combining mark before visible region should NOT be included
+ assert clip('\u0301hello', 1, 4) == 'ell'
+ # Standalone combining mark after visible region should NOT be included
+ assert clip('hello\u0301', 0, 3) == 'hel'
+ # Combining mark within visible region should be included (attached to base)
+ assert clip('he\u0301llo', 0, 4) == 'he\u0301ll'
+
+
+def test_clip_prepend_grapheme():
+ # PREPEND characters (Arabic Number Sign) cluster with following char, width 2
+ # Full cluster fits
+ assert clip('\u0600abc', 0, 2) == '\u0600a'
+ # Cluster split at start boundary - replaced with fillchar
+ assert clip('\u0600abc', 0, 1) == ' '
+ # Cluster split at end boundary - partial overlap gets fillchar
+ assert clip('\u0600abc', 1, 3) == ' b'
+ # Clipping after the prepend cluster
+ assert clip('\u0600abc', 2, 4) == 'bc'
+
+
def test_clip_ambiguous_width_1():
assert clip('\u00b1test', 0, 3, ambiguous_width=1) == '\u00b1te'
@@ -221,3 +255,8 @@ CLIP_CURSOR_SEQUENCE_CASES = [
@pytest.mark.parametrize('text,start,end,expected', CLIP_CURSOR_SEQUENCE_CASES)
def test_clip_cursor_sequences_zero_width(text, start, end, expected):
assert clip(text, start, end) == expected
+
+
+def test_clip_tab_first_visible_with_sgr():
+ """Tab as first visible character with SGR propagation."""
+ assert clip('\x1b[31m\tb', 0, 4, tabsize=8) == '\x1b[31m \x1b[0m'
diff --git a/contrib/python/wcwidth/py3/tests/test_core.py b/contrib/python/wcwidth/py3/tests/test_core.py
index b8ae3611430..fef89f53ce5 100644
--- a/contrib/python/wcwidth/py3/tests/test_core.py
+++ b/contrib/python/wcwidth/py3/tests/test_core.py
@@ -379,20 +379,6 @@ def test_kannada_script_2():
assert length_phrase == expect_length_phrase
-def test_zero_wide_conflict():
- # Test characters considered both "wide" and "zero" width
- # - (0x03000, 0x0303e,), # Ideographic Space ..Ideographic Variation In
- # + (0x03000, 0x03029,), # Ideographic Space ..Hangzhou Numeral Nine
- assert wcwidth.wcwidth(chr(0x03029), unicode_version='4.1.0') == 2
- assert wcwidth.wcwidth(chr(0x0302a), unicode_version='4.1.0') == 0
-
- # - (0x03099, 0x030ff,), # Combining Katakana-hirag..Katakana Digraph Koto
- # + (0x0309b, 0x030ff,), # Katakana-hiragana Voiced..Katakana Digraph Koto
- assert wcwidth.wcwidth(chr(0x03099), unicode_version='4.1.0') == 0
- assert wcwidth.wcwidth(chr(0x0309a), unicode_version='4.1.0') == 0
- assert wcwidth.wcwidth(chr(0x0309b), unicode_version='4.1.0') == 2
-
-
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/contrib/python/wcwidth/py3/tests/test_emojis.py b/contrib/python/wcwidth/py3/tests/test_emojis.py
index 615d6d2f6b5..3f85877dec2 100644
--- a/contrib/python/wcwidth/py3/tests/test_emojis.py
+++ b/contrib/python/wcwidth/py3/tests/test_emojis.py
@@ -188,8 +188,8 @@ def test_recommended_variation_16_sequences(benchmark):
assert len(sequences) >= 742
-def test_unicode_9_vs16():
- """Verify effect of VS-16 on unicode_version 9.0 and later."""
+def test_vs16_effect():
+ """Verify effect of VS-16 (always active with latest Unicode version)."""
phrase = ("\u2640" # FEMALE SIGN
"\uFE0F") # VARIATION SELECTOR-16
@@ -197,25 +197,8 @@ def test_unicode_9_vs16():
expect_length_phrase = 2
# exercise,
- length_each = tuple(wcwidth.wcwidth(w_char, unicode_version='9.0') for w_char in phrase)
- length_phrase = wcwidth.wcswidth(phrase, unicode_version='9.0')
-
- # verify.
- assert length_each == expect_length_each
- assert length_phrase == expect_length_phrase
-
-
-def test_unicode_8_vs16():
- """Verify that VS-16 has no effect on unicode_version 8.0 and earlier."""
- phrase = ("\u2640" # FEMALE SIGN
- "\uFE0F") # VARIATION SELECTOR-16
-
- expect_length_each = (1, 0)
- expect_length_phrase = 1
-
- # exercise,
- length_each = tuple(wcwidth.wcwidth(w_char, unicode_version='8.0') for w_char in phrase)
- length_phrase = wcwidth.wcswidth(phrase, unicode_version='8.0')
+ length_each = tuple(wcwidth.wcwidth(w_char) for w_char in phrase)
+ length_phrase = wcwidth.wcswidth(phrase)
# verify.
assert length_each == expect_length_each
diff --git a/contrib/python/wcwidth/py3/tests/test_sgr_state.py b/contrib/python/wcwidth/py3/tests/test_sgr_state.py
new file mode 100644
index 00000000000..acddfd8ff51
--- /dev/null
+++ b/contrib/python/wcwidth/py3/tests/test_sgr_state.py
@@ -0,0 +1,238 @@
+"""Tests for SGR state tracking and propagation."""
+from __future__ import annotations
+
+# std imports
+import re
+
+# local
+from wcwidth import clip, wrap
+from wcwidth.sgr_state import (_SGR_STATE_DEFAULT,
+ _SGRState,
+ propagate_sgr,
+ _sgr_state_update,
+ _sgr_state_is_active,
+ _sgr_state_to_sequence)
+
+
+def test_wrap_propagates_sgr():
+ """Wrap() propagates SGR codes across lines by default."""
+ assert wrap('\x1b[1;34mHello world\x1b[0m', width=6) == [
+ '\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m']
+ assert wrap('\x1b[1mHello world\x1b[0m', width=6) == [
+ '\x1b[1mHello\x1b[0m', '\x1b[1mworld\x1b[0m']
+ assert wrap('\x1b[31mred text here\x1b[0m', width=5) == [
+ '\x1b[31mred\x1b[0m', '\x1b[31mtext\x1b[0m', '\x1b[31mhere\x1b[0m']
+ assert wrap('\x1b[38;5;208morange text\x1b[0m', width=7) == [
+ '\x1b[38;5;208morange\x1b[0m', '\x1b[38;5;208mtext\x1b[0m']
+ assert wrap('\x1b[38;2;255;0;0mred text\x1b[0m', width=5) == [
+ '\x1b[38;2;255;0;0mred\x1b[0m', '\x1b[38;2;255;0;0mtext\x1b[0m']
+ # multiple attributes combined
+ result = wrap('\x1b[1;3;34mbold italic blue\x1b[0m', width=5)
+ assert result[0] == '\x1b[1;3;34mbold\x1b[0m'
+ assert result[1].startswith('\x1b[') and result[1].endswith('\x1b[0m')
+
+
+def test_wrap_reset_and_no_sgr():
+ """Wrap() handles reset and plain text."""
+ assert wrap('\x1b[31mred\x1b[0m plain text', width=6) == ['\x1b[31mred\x1b[0m', 'plain', 'text']
+ assert wrap('hello world', width=6) == ['hello', 'world']
+
+
+def test_wrap_propagate_sgr_disabled():
+ """Wrap() with propagate_sgr=False returns old behavior."""
+ assert wrap('\x1b[31mhello world\x1b[0m', width=6, propagate_sgr=False) == [
+ '\x1b[31mhello', 'world\x1b[0m']
+
+
+def test_wrap_preserves_non_sgr_sequences():
+ """Wrap() preserves non-SGR sequences (OSC hyperlinks)."""
+ result = wrap('\x1b]8;;url\x07long link text\x1b]8;;\x07', width=5)
+ # Hyperlinks get IDs added for identity preservation across lines
+ osc_pattern = re.compile(r'\x1b]8;[^;]*;url\x07')
+ assert all(osc_pattern.search(line) for line in result)
+
+
+def test_clip_propagates_sgr():
+ """Clip() restores SGR state at start and reset at end."""
+ assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11) == '\x1b[1;34mworld\x1b[0m'
+ assert clip('\x1b[1mHello world\x1b[0m', 6, 11) == '\x1b[1mworld\x1b[0m'
+ assert clip('\x1b[31mHello world\x1b[0m', 6, 11) == '\x1b[31mworld\x1b[0m'
+ assert clip('\x1b[31mHello\x1b[0m', 0, 5) == '\x1b[31mHello\x1b[0m'
+
+
+def test_clip_no_active_style_and_plain():
+ """Clip() handles reset and plain text."""
+ assert clip('\x1b[31mred\x1b[0m plain', 4, 9) == 'plain'
+ assert clip('Hello world', 6, 11) == 'world'
+
+
+def test_clip_propagate_sgr_disabled():
+ """Clip() with propagate_sgr=False returns old behavior."""
+ assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11, propagate_sgr=False) == '\x1b[1;34mworld\x1b[0m'
+
+
+def test_clip_preserves_non_sgr_sequences():
+ """Clip() preserves non-SGR sequences (OSC hyperlinks)."""
+ result = clip('\x1b]8;;url\x07link\x1b]8;;\x07', 0, 4)
+ assert '\x1b]8;;url\x07' in result and '\x1b]8;;\x07' in result
+
+
+def test_clip_sgr_only_no_visible_content():
+ """Clip() returns empty when only SGR sequences exist."""
+ assert clip('\x1b[31m\x1b[0m', 0, 10) == ''
+
+
+def test_sgr_state_parse_boolean_attributes_on():
+ """_sgr_state_update parses all boolean attribute on codes."""
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1m').bold is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[2m').dim is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[3m').italic is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[4m').underline is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[5m').blink is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[6m').rapid_blink is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[7m').inverse is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[8m').hidden is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[9m').strikethrough is True
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[21m').double_underline is True
+
+
+def test_sgr_state_parse_boolean_attributes_off():
+ """_sgr_state_update parses all boolean attribute off codes."""
+ assert _sgr_state_update(_SGRState(italic=True), '\x1b[23m').italic is False
+ assert _sgr_state_update(_SGRState(inverse=True), '\x1b[27m').inverse is False
+ assert _sgr_state_update(_SGRState(hidden=True), '\x1b[28m').hidden is False
+ assert _sgr_state_update(_SGRState(strikethrough=True), '\x1b[29m').strikethrough is False
+ # code 22 resets both bold and dim
+ state = _sgr_state_update(_SGRState(bold=True, dim=True), '\x1b[22m')
+ assert state.bold is False and state.dim is False
+ # code 24 resets both underline and double_underline
+ state = _sgr_state_update(_SGRState(underline=True, double_underline=True), '\x1b[24m')
+ assert state.underline is False and state.double_underline is False
+ # code 25 resets both blink and rapid_blink
+ state = _sgr_state_update(_SGRState(blink=True, rapid_blink=True), '\x1b[25m')
+ assert state.blink is False and state.rapid_blink is False
+
+
+def test_sgr_state_parse_colors():
+ """_sgr_state_update parses all color formats."""
+ # basic foreground/background
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[31m').foreground == (31,)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[41m').background == (41,)
+ # bright foreground/background
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[91m').foreground == (91,)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[101m').background == (101,)
+ # 256-color (semicolon format)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m').foreground == (38, 5, 208)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5;208m').background == (48, 5, 208)
+ # RGB (semicolon format)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255;128;0m').foreground == (38, 2, 255, 128, 0)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255;128;0m').background == (48, 2, 255, 128, 0)
+
+
+def test_sgr_state_parse_colors_colon_format():
+ """_sgr_state_update parses ITU T.416 colon-separated color format."""
+ # 256-color (colon format)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:5:208m').foreground == (38, 5, 208)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:5:208m').background == (48, 5, 208)
+ # RGB with empty colorspace (colon format): 38:2::R:G:B
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2::255:128:0m').foreground == (38, 2, 0, 255, 128, 0)
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48:2::255:128:0m').background == (48, 2, 0, 255, 128, 0)
+ # RGB with colorspace (colon format): 38:2:colorspace:R:G:B
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38:2:1:255:128:0m').foreground == (38, 2, 1, 255, 128, 0)
+ # Mixed: colon color with semicolon attributes
+ state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;38:2::255:0:0;4m')
+ assert state.bold is True
+ assert state.underline is True
+ assert state.foreground == (38, 2, 0, 255, 0, 0)
+
+
+def test_sgr_state_color_override():
+ """Newer color replaces older regardless of format."""
+ state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5;208m')
+ assert state.foreground == (38, 5, 208)
+ state = _sgr_state_update(state, '\x1b[31m')
+ assert state.foreground == (31,)
+ state = _sgr_state_update(state, '\x1b[38;2;0;255;0m')
+ assert state.foreground == (38, 2, 0, 255, 0)
+ state = _sgr_state_update(state, '\x1b[38;5;99m')
+ assert state.foreground == (38, 5, 99)
+
+
+def test_sgr_state_parse_default_colors():
+ """_sgr_state_update parses default color codes (39, 49)."""
+ assert _sgr_state_update(_SGRState(foreground=(31,)), '\x1b[39m').foreground is None
+ assert _sgr_state_update(_SGRState(background=(41,)), '\x1b[49m').background is None
+
+
+def test_sgr_state_parse_compound_and_reset():
+ """_sgr_state_update handles compound sequences and reset."""
+ state = _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[1;34;3m')
+ assert state.bold is True and state.italic is True and state.foreground == (34,)
+ # reset with 0
+ state = _sgr_state_update(_SGRState(bold=True, foreground=(31,)), '\x1b[0m')
+ assert state == _SGR_STATE_DEFAULT
+ # empty is reset
+ state = _sgr_state_update(_SGRState(bold=True), '\x1b[m')
+ assert state == _SGR_STATE_DEFAULT
+ # empty param in compound treated as 0
+ state = _sgr_state_update(_SGRState(bold=True), '\x1b[;1m')
+ assert state.bold is True
+
+
+def test_sgr_state_to_sequence():
+ """_sgr_state_to_sequence generates correct sequences."""
+ assert _sgr_state_to_sequence(_SGR_STATE_DEFAULT) == ''
+ assert _sgr_state_to_sequence(_SGRState(bold=True)) == '\x1b[1m'
+ assert _sgr_state_to_sequence(_SGRState(rapid_blink=True)) == '\x1b[6m'
+ assert _sgr_state_to_sequence(_SGRState(double_underline=True)) == '\x1b[21m'
+ assert _sgr_state_to_sequence(_SGRState(foreground=(31,))) == '\x1b[31m'
+ assert _sgr_state_to_sequence(_SGRState(foreground=(38, 5, 208))) == '\x1b[38;5;208m'
+ assert _sgr_state_to_sequence(_SGRState(foreground=(38, 2, 255, 128, 0))) == '\x1b[38;2;255;128;0m'
+ assert _sgr_state_to_sequence(_SGRState(bold=True, italic=True, foreground=(34,))) == '\x1b[1;3;34m'
+
+
+def test_sgr_state_to_sequence_all_attributes():
+ """_sgr_state_to_sequence handles all attributes."""
+ state = _SGRState(bold=True, italic=True, underline=True, inverse=True,
+ foreground=(31,), background=(44,))
+ seq = _sgr_state_to_sequence(state)
+ assert '\x1b[' in seq and 'm' in seq
+ assert all(code in seq for code in ('1', '3', '4', '7', '31', '44'))
+
+
+def test_sgr_state_is_active():
+ """_sgr_state_is_active detects active state."""
+ assert _sgr_state_is_active(_SGR_STATE_DEFAULT) is False
+ assert _sgr_state_is_active(_SGRState(bold=True)) is True
+ assert _sgr_state_is_active(_SGRState(foreground=(31,))) is True
+
+
+def test_propagate_sgr():
+ """propagate_sgr handles various input cases."""
+ assert len(propagate_sgr([])) == 0
+ assert propagate_sgr(['hello', 'world']) == ['hello', 'world']
+ assert propagate_sgr(['\x1b[31mhello\x1b[0m']) == ['\x1b[31mhello\x1b[0m']
+ assert propagate_sgr(['\x1b[31mhello', 'world\x1b[0m']) == [
+ '\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']
+ assert propagate_sgr(['\x1b[31mred\x1b[0m', 'plain']) == ['\x1b[31mred\x1b[0m', 'plain']
+
+
+def test_propagate_sgr_empty_lines():
+ """propagate_sgr handles empty lines."""
+ result = propagate_sgr(['\x1b[31mred', '', 'text\x1b[0m'])
+ assert result[0] == '\x1b[31mred\x1b[0m'
+ assert result[1] == '\x1b[31m\x1b[0m'
+ assert result[2] == '\x1b[31mtext\x1b[0m'
+
+
+def test_sgr_malformed_sequences():
+ """Malformed SGR sequences are ignored."""
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;5m').foreground is None
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;2;255m').foreground is None
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[999m') == _SGR_STATE_DEFAULT
+ # malformed background extended colors
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;5m').background is None
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;2;255m').background is None
+ # invalid mode (not 2 or 5) for extended color
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[38;3;128m').foreground is None
+ assert _sgr_state_update(_SGR_STATE_DEFAULT, '\x1b[48;3;128m').background is None
diff --git a/contrib/python/wcwidth/py3/tests/test_textwrap.py b/contrib/python/wcwidth/py3/tests/test_textwrap.py
index fc15f1917f9..eb12621ba3f 100644
--- a/contrib/python/wcwidth/py3/tests/test_textwrap.py
+++ b/contrib/python/wcwidth/py3/tests/test_textwrap.py
@@ -11,6 +11,20 @@ import pytest
from wcwidth import iter_sequences
from wcwidth.textwrap import SequenceTextWrapper, wrap
+
[email protected](autouse=True)
+def mock_hyperlink_ids(monkeypatch):
+ """Mock secrets.token_hex to return predictable IDs for testing."""
+ counter = 0
+
+ def fake_token_hex(n):
+ nonlocal counter
+ counter += 1
+ return f'{counter:0{n * 2}x}'
+
+ monkeypatch.setattr('secrets.token_hex', fake_token_hex)
+
+
SGR_RED = '\x1b[31m'
SGR_BLUE = '\x1b[34m'
SGR_BOLD = '\x1b[1m'
@@ -103,6 +117,12 @@ HYPHEN_LONG_WORD_CASES = [
('a-b-c-d', 3, False, ['a-b', '-c-', 'd']),
('---', 2, True, ['--', '-']),
('a---b', 2, True, ['a-', '--', 'b']),
+ # With propagate_sgr=True, SGR continues to next line
+ ('a-\x1b[31mb', 2, True, ['a-\x1b[31m\x1b[0m', '\x1b[31mb\x1b[0m']),
+]
+
+HYPHEN_LONG_WORD_CASES_NO_PROPAGATE = [
+ # With propagate_sgr=False, SGR stays where it is
('a-\x1b[31mb', 2, True, ['a-\x1b[31m', 'b']),
]
@@ -112,6 +132,11 @@ def test_wrap_hyphen_long_words(text, w, break_hyphens, expected):
assert wrap(text, w, break_on_hyphens=break_hyphens) == expected
[email protected]('text,w,break_hyphens,expected', HYPHEN_LONG_WORD_CASES_NO_PROPAGATE)
+def test_wrap_hyphen_long_words_no_propagate(text, w, break_hyphens, expected):
+ assert wrap(text, w, break_on_hyphens=break_hyphens, propagate_sgr=False) == expected
+
+
# Comprehensive stdlib compatibility
TEXTWRAP_KWARGS = [
{'break_long_words': False, 'drop_whitespace': False},
@@ -196,24 +221,37 @@ def test_wrap_unicode(benchmark, text, w, expected):
assert result == expected
-# Escape sequence preservation
+# Escape sequence preservation (with propagate_sgr=True default)
SEQUENCE_CASES = [
- # SGR sequences preserved at word boundaries
+ # SGR sequences propagated across lines
(f'{SGR_RED}red{SGR_RESET} blue', 4, [f'{SGR_RED}red{SGR_RESET}', 'blue']),
- (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}', 'world']),
- # Empty/adjacent sequences
+ # SGR at end of line propagates to next line
+ (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}{SGR_RESET}', f'{SGR_RED}world{SGR_RESET}']),
+ # Empty/adjacent sequences - sequences kept (no visible content)
(f'{SGR_RED}{SGR_RESET}', 10, [f'{SGR_RED}{SGR_RESET}']),
+ # Reset clears style, sequences kept with second line
(f'hello {SGR_RED}{SGR_RESET}world', 6, ['hello', f'{SGR_RED}{SGR_RESET}world']),
- # OSC hyperlinks (with space separator)
+ # OSC hyperlinks (with space separator) - not SGR, so preserved as-is
(f'{OSC_HYPERLINK} text', 5, [OSC_HYPERLINK, 'text']),
- # CSI cursor sequences
+ # CSI cursor sequences - not SGR, so preserved as-is
(f'{CSI_CURSOR}text here', 10, [f'{CSI_CURSOR}text', 'here']),
# Control characters
(f'{CTRL_BEL}alert text', 6, [f'{CTRL_BEL}alert', 'text']),
- # Sequences in long word breaking
- ('x\x1b[31mabcdefghij\x1b[0m', 3, ['xab', 'cde', 'fgh', 'ij']),
- # Lone ESC
- ('abc\x1bdefghij', 3, ['abc', 'def', 'ghi', 'j']),
+ # Sequences in long word breaking - red starts after 'x', continues across lines
+ ('x\x1b[31mabcdefghij\x1b[0m', 3,
+ ['x\x1b[31mab\x1b[0m', '\x1b[31mcde\x1b[0m', '\x1b[31mfgh\x1b[0m', '\x1b[31mij\x1b[0m']),
+ # Lone ESC - not a valid SGR sequence, stays with preceding text
+ ('abc\x1bdefghij', 3, ['abc\x1b', 'def', 'ghi', 'j']),
+]
+
+# Old behavior tests (propagate_sgr=False)
+SEQUENCE_CASES_NO_PROPAGATE = [
+ (f'{SGR_RED}red{SGR_RESET} blue', 4, [f'{SGR_RED}red{SGR_RESET}', 'blue']),
+ (f'hello{SGR_RED} world', 6, [f'hello{SGR_RED}', 'world']),
+ (f'{SGR_RED}{SGR_RESET}', 10, [f'{SGR_RED}{SGR_RESET}']),
+ (f'hello {SGR_RED}{SGR_RESET}world', 6, ['hello', f'{SGR_RED}{SGR_RESET}world']),
+ # Sequences preserved where they are, not propagated
+ ('x\x1b[31mabcdefghij\x1b[0m', 3, ['x\x1b[31mab', 'cde', 'fgh', 'ij\x1b[0m']),
]
@@ -223,7 +261,13 @@ def test_wrap_sequences(benchmark, text, w, expected):
if any('\x1b' in e or '\x00' <= e[0] < '\x20' for e in expected if e):
assert result == expected
else:
- assert [_strip(line) for line in result] == expected
+ assert result == expected
+
+
[email protected]('text,w,expected', SEQUENCE_CASES_NO_PROPAGATE)
+def test_wrap_sequences_no_propagate(text, w, expected):
+ result = wrap(text, w, propagate_sgr=False)
+ assert result == expected
# Mixed: sequences + unicode
@@ -321,6 +365,78 @@ HYPERLINK_WORD_BOUNDARY_CASES = [
6,
['foo', f'{OSC_START_BEL}{SGR_RED}link{SGR_RESET}{OSC_END_BEL}', 'bar'],
),
+ ( # hyperlink with internal space - breaks with id continuation (ST)
+ f'Go {OSC_START_ST}Click here{OSC_END_ST} now',
+ 5,
+ [
+ 'Go',
+ '\x1b]8;id=00000001;http://example.com\x1b\\Click\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\here\x1b]8;;\x1b\\',
+ 'now',
+ ],
+ ),
+ ( # hyperlink with internal space - breaks with id continuation (BEL)
+ f'Go {OSC_START_BEL}Click here{OSC_END_BEL} now',
+ 5,
+ [
+ 'Go',
+ '\x1b]8;id=00000001;http://example.com\x07Click\x1b]8;;\x07',
+ '\x1b]8;id=00000001;http://example.com\x07here\x1b]8;;\x07',
+ 'now',
+ ],
+ ),
+ ( # hyperlink with existing id= parameter is preserved
+ '\x1b]8;id=my-link;http://example.com\x1b\\Click here\x1b]8;;\x1b\\',
+ 6,
+ [
+ '\x1b]8;id=my-link;http://example.com\x1b\\Click\x1b]8;;\x1b\\',
+ '\x1b]8;id=my-link;http://example.com\x1b\\here\x1b]8;;\x1b\\',
+ ],
+ ),
+ ( # hyperlink spanning 3+ lines
+ f'{OSC_START_ST}one two three{OSC_END_ST}',
+ 5,
+ [
+ '\x1b]8;id=00000001;http://example.com\x1b\\one\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\two\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\three\x1b]8;;\x1b\\',
+ ],
+ ),
+ ( # multiple hyperlinks in same text
+ f'{OSC_START_ST}ab cd{OSC_END_ST} {OSC_START_BEL}ef gh{OSC_END_BEL}',
+ 4,
+ [
+ '\x1b]8;id=00000001;http://example.com\x1b\\ab\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\cd\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000002;http://example.com\x07ef\x1b]8;;\x07',
+ '\x1b]8;id=00000002;http://example.com\x07gh\x1b]8;;\x07',
+ ],
+ ),
+ ( # long word inside hyperlink forces character-level breaking
+ f'{OSC_START_ST}abcdefgh{OSC_END_ST}',
+ 3,
+ [
+ '\x1b]8;id=00000001;http://example.com\x1b\\abc\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\def\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001;http://example.com\x1b\\gh\x1b]8;;\x1b\\',
+ ],
+ ),
+ ( # params with other keys but no id - id is prepended, other params preserved
+ '\x1b]8;foo=bar;http://example.com\x1b\\Click here\x1b]8;;\x1b\\',
+ 6,
+ [
+ '\x1b]8;id=00000001:foo=bar;http://example.com\x1b\\Click\x1b]8;;\x1b\\',
+ '\x1b]8;id=00000001:foo=bar;http://example.com\x1b\\here\x1b]8;;\x1b\\',
+ ],
+ ),
+ ( # id not at start of params (junk:id=given) - full params preserved
+ '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\Click here\x1b]8;;\x1b\\',
+ 6,
+ [
+ '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\Click\x1b]8;;\x1b\\',
+ '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\here\x1b]8;;\x1b\\',
+ ],
+ ),
]
diff --git a/contrib/python/wcwidth/py3/tests/test_ucslevel.py b/contrib/python/wcwidth/py3/tests/test_ucslevel.py
index a907db2c44a..979cfe0fe8d 100644
--- a/contrib/python/wcwidth/py3/tests/test_ucslevel.py
+++ b/contrib/python/wcwidth/py3/tests/test_ucslevel.py
@@ -1,189 +1,58 @@
"""Unicode version level tests for wcwidth."""
-# std imports
-import warnings
-
-# 3rd party
-import pytest
-
# local
import wcwidth
+def test_list_versions_single():
+ """list_versions returns only the latest version."""
+ versions = wcwidth.list_versions()
+ assert len(versions) == 1
+ assert versions[0] == "17.0.0"
+
+
def test_latest():
- """wcwidth._wcmatch_version('latest') returns tail item."""
- # given,
+ """wcwidth._wcmatch_version('latest') returns the latest version."""
expected = wcwidth.list_versions()[-1]
-
- # exercise,
result = wcwidth._wcmatch_version('latest')
-
- # verify.
- assert result == expected
-
-
-def test_exact_410_str():
- """wcwidth._wcmatch_version('4.1.0') returns equal value (str)."""
- # given,
- given = expected = '4.1.0'
-
- # exercise,
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_exact_410_unicode():
- """wcwidth._wcmatch_version(u'4.1.0') returns equal value (unicode)."""
- # given,
- given = expected = '4.1.0'
-
- # exercise,
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_505_str():
- """
- wcwidth._wcmatch_version('5.0.5') returns nearest '5.0.0'.
-
- (str)
- """
- # given
- given, expected = '5.0.5', '5.0.0'
-
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_505_unicode():
- """
- wcwidth._wcmatch_version(u'5.0.5') returns nearest u'5.0.0'.
-
- (unicode)
- """
- # given
- given, expected = '5.0.5', '5.0.0'
-
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_lowint40_str():
- """wcwidth._wcmatch_version('4.0') returns nearest '4.1.0'."""
- # given
- given, expected = '4.0', '4.1.0'
- warnings.resetwarnings()
- wcwidth._wcmatch_version.cache_clear()
-
- # exercise
- with pytest.warns(UserWarning):
- # warns that given version is lower than any available
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_lowint40_unicode():
- """wcwidth._wcmatch_version(u'4.0') returns nearest u'4.1.0'."""
- # given
- given, expected = '4.0', '4.1.0'
- warnings.resetwarnings()
- wcwidth._wcmatch_version.cache_clear()
-
- # exercise
- with pytest.warns(UserWarning):
- # warns that given version is lower than any available
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_800_str():
- """wcwidth._wcmatch_version('8') returns nearest '8.0.0'."""
- # given
- given, expected = '8', '8.0.0'
-
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
assert result == expected
-def test_nearest_800_unicode():
- """wcwidth._wcmatch_version(u'8') returns nearest u'8.0.0'."""
- # given
- given, expected = '8', '8.0.0'
-
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-
-def test_nearest_999_str():
- """wcwidth._wcmatch_version('999.0') returns nearest (latest)."""
- # given
- given, expected = '999.0', wcwidth.list_versions()[-1]
-
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
+def test_auto():
+ """wcwidth._wcmatch_version('auto') returns the latest version."""
+ expected = wcwidth.list_versions()[-1]
+ result = wcwidth._wcmatch_version('auto')
assert result == expected
-def test_nearest_999_unicode():
- """wcwidth._wcmatch_version(u'999.0') returns nearest (latest)."""
- # given
- given, expected = '999.0', wcwidth.list_versions()[-1]
+def test_wcmatch_version_always_latest():
+ """_wcmatch_version always returns latest regardless of input."""
+ latest = wcwidth.list_versions()[-1]
+ assert wcwidth._wcmatch_version('4.1.0') == latest
+ assert wcwidth._wcmatch_version('9.0.0') == latest
+ assert wcwidth._wcmatch_version('auto') == latest
+ assert wcwidth._wcmatch_version('latest') == latest
+ assert wcwidth._wcmatch_version('invalid') == latest
+ assert wcwidth._wcmatch_version('999.0.0') == latest
+ assert wcwidth._wcmatch_version('x.y.z') == latest
+ assert wcwidth._wcmatch_version('8') == latest
+ assert wcwidth._wcmatch_version('5.0.5') == latest
- # exercise
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
-
-def test_nonint_unicode():
- """wcwidth._wcmatch_version(u'x.y.z') returns latest (unicode)."""
- # given
- given, expected = 'x.y.z', wcwidth.list_versions()[-1]
- warnings.resetwarnings()
+def test_env_var_ignored(monkeypatch):
+ """UNICODE_VERSION environment variable is ignored."""
+ monkeypatch.setenv('UNICODE_VERSION', '4.1.0')
wcwidth._wcmatch_version.cache_clear()
+ assert wcwidth._wcmatch_version('auto') == wcwidth.list_versions()[-1]
- # exercise
- with pytest.warns(UserWarning):
- # warns that given version is not valid
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
+def test_unicode_version_param_ignored():
+ """unicode_version parameter is ignored in wcwidth/wcswidth."""
+ # wcwidth should work the same regardless of unicode_version value
+ assert wcwidth.wcwidth('A', unicode_version='4.1.0') == 1
+ assert wcwidth.wcwidth('A', unicode_version='latest') == 1
+ assert wcwidth.wcwidth('A', unicode_version='invalid') == 1
-def test_nonint_str():
- """wcwidth._wcmatch_version(u'x.y.z') returns latest (str)."""
- # given
- given, expected = 'x.y.z', wcwidth.list_versions()[-1]
- warnings.resetwarnings()
- wcwidth._wcmatch_version.cache_clear()
-
- # exercise
- with pytest.warns(UserWarning):
- # warns that given version is not valid
- result = wcwidth._wcmatch_version(given)
-
- # verify.
- assert result == expected
+ # wcswidth should work the same regardless of unicode_version value
+ assert wcwidth.wcswidth('hello', unicode_version='4.1.0') == 5
+ assert wcwidth.wcswidth('hello', unicode_version='latest') == 5
+ assert wcwidth.wcswidth('hello', unicode_version='invalid') == 5