From eb858216113c4968059470fa906dea7df66beaeb Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 21 Sep 2026 13:57:23 -0700 Subject: [PATCH 1/8] gh-#157928: PyREPL Detect OSC sequences --- Lib/_pyrepl/utils.py | 4 +++- Lib/test/test_pyrepl/test_render.py | 9 +++++++++ Lib/test/test_pyrepl/test_utils.py | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index e77001f6a1d10b..fd63fadb6072bd 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -19,7 +19,9 @@ from .trace import trace -ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +ANSI_ESCAPE_SEQUENCE = re.compile( + r"\x1b(?:\[[ -@]*[A-~]|\][^\x1b\x07]*(?:\x07|\x1b\\))" +) ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) IDENTIFIERS_AFTER = frozenset({"def", "class"}) diff --git a/Lib/test/test_pyrepl/test_render.py b/Lib/test/test_pyrepl/test_render.py index 5479f2eb4bae39..10a119150376a0 100644 --- a/Lib/test/test_pyrepl/test_render.py +++ b/Lib/test/test_pyrepl/test_render.py @@ -55,6 +55,15 @@ def test_from_rendered_text_empty_string(self): self.assertEqual(line.text, "") self.assertEqual(line.width, 0) + def test_from_rendered_text_with_osc_control(self): + osc = "\x1b]633;A\x1b\\" + line = RenderLine.from_rendered_text(f"{osc}>>> ") + + self.assertEqual(line.width, 4) + self.assertEqual(line.cells[0].controls, (osc,)) + self.assertEqual(line.cells[0].text, "") + self.assertEqual(line.text, f"{osc}>>> ") + def test_from_rendered_text_with_non_sgr_controls(self): # \x1b[H is a cursor-home control (not SGR since it doesn't end with 'm') line = RenderLine.from_rendered_text("\x1b[Hx") diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index e87d14304c8225..e0e809838279ea 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -47,6 +47,9 @@ def test_wlen(self): self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1) self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2) + def test_wlen_with_osc_sequence(self): + self.assertEqual(wlen("\x1b]633;A\x07>>> "), 4) + def test_prev_next_window(self): def gen_normal(): yield 1 From d88732f55ab155a858e3993c78acd9e00858e686 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:07:13 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst diff --git a/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst b/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst new file mode 100644 index 00000000000000..2fa00484bf8986 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst @@ -0,0 +1 @@ +Correctly detect and handle OSC sequences in PyREPL From 995f364b7c828fa881f4d8955ffad3a3047b4379 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 21 Sep 2026 14:10:23 -0700 Subject: [PATCH 3/8] Add negative test --- Lib/test/test_pyrepl/test_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index e0e809838279ea..13483937831b25 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -50,6 +50,9 @@ def test_wlen(self): def test_wlen_with_osc_sequence(self): self.assertEqual(wlen("\x1b]633;A\x07>>> "), 4) + def test_wlen_with_unterminated_osc_sequence(self): + self.assertEqual(wlen("\x1b]633;A"), 7) + def test_prev_next_window(self): def gen_normal(): yield 1 From 81df8f9a80daf0b5aaa3441f5554b48314178768 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 1 Oct 2026 16:47:32 -0700 Subject: [PATCH 4/8] Apply batched suggestions from code review Co-authored-by: Brett Cannon --- Lib/test/test_pyrepl/test_render.py | 6 +++--- Lib/test/test_pyrepl/test_utils.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_pyrepl/test_render.py b/Lib/test/test_pyrepl/test_render.py index 10a119150376a0..7293f0f33f7405 100644 --- a/Lib/test/test_pyrepl/test_render.py +++ b/Lib/test/test_pyrepl/test_render.py @@ -57,12 +57,12 @@ def test_from_rendered_text_empty_string(self): def test_from_rendered_text_with_osc_control(self): osc = "\x1b]633;A\x1b\\" - line = RenderLine.from_rendered_text(f"{osc}>>> ") + prompt = ">>> " + line = RenderLine.from_rendered_text(osc + prompt) - self.assertEqual(line.width, 4) + self.assertEqual(line.width, len(prompt)) self.assertEqual(line.cells[0].controls, (osc,)) self.assertEqual(line.cells[0].text, "") - self.assertEqual(line.text, f"{osc}>>> ") def test_from_rendered_text_with_non_sgr_controls(self): # \x1b[H is a cursor-home control (not SGR since it doesn't end with 'm') diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index 13483937831b25..176e5b4e7deef0 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -48,7 +48,8 @@ def test_wlen(self): self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2) def test_wlen_with_osc_sequence(self): - self.assertEqual(wlen("\x1b]633;A\x07>>> "), 4) + prompt = ">>> " + self.assertEqual(wlen("\x1b]633;A\x07" + prompt), len(prompt)) def test_wlen_with_unterminated_osc_sequence(self): self.assertEqual(wlen("\x1b]633;A"), 7) From 4c1df46bf9d802d74a65c67e6961607dbb4f8ecf Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 1 Oct 2026 16:58:33 -0700 Subject: [PATCH 5/8] Update ANSI_ESCAPE_SEQUENCE comments --- Lib/_pyrepl/utils.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index fd63fadb6072bd..180e111873dbb3 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -20,7 +20,19 @@ ANSI_ESCAPE_SEQUENCE = re.compile( - r"\x1b(?:\[[ -@]*[A-~]|\][^\x1b\x07]*(?:\x07|\x1b\\))" + r""" + \x1b + (?: + # CSI: ESC [ followed by parameter/intermediate bytes and a final byte. + # For example, ESC [ 31 m ("\x1b[31m") selects red foreground text. + \[ [ -@]* [A-~] + | + # OSC: ESC ] followed by a payload terminated by BEL or ST (ESC \). + # For example, ESC ] 0 ; title BEL ("\x1b]0;title\x07") sets the title. + \] [^\x1b\x07]* (?: \x07 | \x1b\\ ) + ) + """, + re.X, ) ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) From afbef18f81c03f197b260b30f0f2b5b991f8f389 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 1 Oct 2026 17:01:56 -0700 Subject: [PATCH 6/8] Apply suggestion from @brettcannon Co-authored-by: Brett Cannon --- .../next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst b/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst index 2fa00484bf8986..dfa7a122a9e22e 100644 --- a/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst +++ b/Misc/NEWS.d/next/Library/2026-09-21-21-07-12.gh-issue-157928.ZPlNYC.rst @@ -1 +1 @@ -Correctly detect and handle OSC sequences in PyREPL +Correctly detect and handle OSC sequences in PyREPL. From 97ba530011158409d1effea0476748cf6119205b Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Thu, 1 Oct 2026 17:04:44 -0700 Subject: [PATCH 7/8] Update test with clearer explanation --- Lib/test/test_pyrepl/test_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index 176e5b4e7deef0..1cf7cb4d101307 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -51,7 +51,8 @@ def test_wlen_with_osc_sequence(self): prompt = ">>> " self.assertEqual(wlen("\x1b]633;A\x07" + prompt), len(prompt)) - def test_wlen_with_unterminated_osc_sequence(self): + def test_wlen_with_unterminated_osc_sequence_as_individual_characters(self): + # \x1b is treated as a single character (ESC) self.assertEqual(wlen("\x1b]633;A"), 7) def test_prev_next_window(self): From 46c325631be399bba3ba07ccee001503f3095205 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Fri, 2 Oct 2026 09:54:54 -0700 Subject: [PATCH 8/8] Update test --- Lib/test/test_pyrepl/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index 1cf7cb4d101307..5c60770c511c07 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -53,7 +53,7 @@ def test_wlen_with_osc_sequence(self): def test_wlen_with_unterminated_osc_sequence_as_individual_characters(self): # \x1b is treated as a single character (ESC) - self.assertEqual(wlen("\x1b]633;A"), 7) + self.assertEqual(wlen("\x1b]633;A"), len("\x1b]633;A")) def test_prev_next_window(self): def gen_normal():