From e5f42604e7976a34674062047f3caac6c5468891 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Wed, 28 May 2025 22:19:18 -0500 Subject: [PATCH 01/20] fix a dos --- Lib/idlelib/editor.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index c76db20c58792d..70f09518efb3f4 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1203,10 +1203,20 @@ def load_extension(self, name): self.apply_bindings(keydefs) for vevent in keydefs: methodname = vevent.replace("-", "_") - while methodname[:1] == '<': - methodname = methodname[1:] - while methodname[-1:] == '>': - methodname = methodname[:-1] + stripl = 0 + for char in methodname: + if char == '<': + stripl += 1 + else: + break + methodname = methodname[stripl:] + stripr = 0 + for char in methodname[::-1]: + if char == '>': + stripr -= 1 + else: + break + methodname = methodname[:stripr] methodname = methodname + "_event" if hasattr(ins, methodname): self.text.bind(vevent, getattr(ins, methodname)) From aa5b5a813fefe435fbde492c0a30324b3148bcac Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 03:24:19 +0000 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst diff --git a/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst new file mode 100644 index 00000000000000..cac6108a74d62a --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst @@ -0,0 +1 @@ +A DOS vulnerability in ``idlelib`` is fixed regarding string slicing. From edfa9042cf13e5d8d951119ffa53c70b423b04c1 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 29 May 2025 21:41:04 -0500 Subject: [PATCH 03/20] Update Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst Co-authored-by: Peter Bierma --- .../Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst index cac6108a74d62a..389b8af30ef048 100644 --- a/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst +++ b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst @@ -1 +1 @@ -A DOS vulnerability in ``idlelib`` is fixed regarding string slicing. +Fix a DOS vulnerability in :mod:`idlelib` regarding string slicing. From 2090bafb8f67a927703b025fa34ec347b5eb6b79 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Thu, 29 May 2025 22:29:53 -0500 Subject: [PATCH 04/20] another dos and also simplify --- Lib/idlelib/editor.py | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 70f09518efb3f4..e4024839bea55f 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1203,20 +1203,7 @@ def load_extension(self, name): self.apply_bindings(keydefs) for vevent in keydefs: methodname = vevent.replace("-", "_") - stripl = 0 - for char in methodname: - if char == '<': - stripl += 1 - else: - break - methodname = methodname[stripl:] - stripr = 0 - for char in methodname[::-1]: - if char == '>': - stripr -= 1 - else: - break - methodname = methodname[:stripr] + methodname = methodname.lstrip('<').rstrip('>') methodname = methodname + "_event" if hasattr(ins, methodname): self.text.bind(vevent, getattr(ins, methodname)) @@ -1378,14 +1365,18 @@ def smart_backspace_event(self, event): have = len(chars.expandtabs(tabwidth)) assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth - # Debug prompt is multilined.... + ncharsdeleted = 0 - while True: - chars = chars[:-1] - ncharsdeleted = ncharsdeleted + 1 - have = len(chars.expandtabs(tabwidth)) - if have <= want or chars[-1] not in " \t": + have = len(chars.expandtabs(tabwidth)) + + for i in range(len(chars) - 1, -1, -1): + if have <= want or chars[i] not in " \t": break + ncharsdeleted += 1 + + chars = chars[:len(chars) - ncharsdeleted] + have = len(chars.expandtabs(tabwidth)) + text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: From d2838a589b9aa90d04bd50e40176e8812215324d Mon Sep 17 00:00:00 2001 From: John Zhou Date: Thu, 29 May 2025 22:34:27 -0500 Subject: [PATCH 05/20] grumble --- Lib/idlelib/editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index e4024839bea55f..2aebd528ebfbdc 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1365,7 +1365,7 @@ def smart_backspace_event(self, event): have = len(chars.expandtabs(tabwidth)) assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth - + ncharsdeleted = 0 have = len(chars.expandtabs(tabwidth)) @@ -1373,7 +1373,7 @@ def smart_backspace_event(self, event): if have <= want or chars[i] not in " \t": break ncharsdeleted += 1 - + chars = chars[:len(chars) - ncharsdeleted] have = len(chars.expandtabs(tabwidth)) From 83bafff6744aff00d78bf21a8e0ae71b7d30a38e Mon Sep 17 00:00:00 2001 From: John Zhou Date: Fri, 30 May 2025 10:41:02 -0500 Subject: [PATCH 06/20] have condition --- Lib/idlelib/editor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 2aebd528ebfbdc..1cc0c4468b62f2 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1366,16 +1366,14 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth + # Debug prompt is multilined.... ncharsdeleted = 0 - have = len(chars.expandtabs(tabwidth)) - for i in range(len(chars) - 1, -1, -1): + have = len(chars.expandtabs(tabwidth)) if have <= want or chars[i] not in " \t": break ncharsdeleted += 1 - chars = chars[:len(chars) - ncharsdeleted] - have = len(chars.expandtabs(tabwidth)) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") From 660fbd388c9154ed7f2e56ee9902198f1599c1be Mon Sep 17 00:00:00 2001 From: John Zhou Date: Fri, 30 May 2025 11:04:37 -0500 Subject: [PATCH 07/20] significant refactor --- Lib/idlelib/editor.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 1cc0c4468b62f2..74ad176ed47831 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1368,11 +1368,18 @@ def smart_backspace_event(self, event): # Debug prompt is multilined.... ncharsdeleted = 0 + have = len(chars.expandtabs(tabwidth)) for i in range(len(chars) - 1, -1, -1): - have = len(chars.expandtabs(tabwidth)) - if have <= want or chars[i] not in " \t": - break + # ``Delete'' chars[i], and subtract count + # (since redoing expandtabs is O(n)) ncharsdeleted += 1 + if chars[i] == '\t': + have -= tabwidth + else: + have -= 1 + if have <= want or chars[i-1] not in " \t": + break + # Perform the actual removal chars = chars[:len(chars) - ncharsdeleted] text.undo_block_start() From 15f821a8daaeecf24b548d8468f1de5a8cb6c232 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 1 Jun 2025 11:45:34 -0500 Subject: [PATCH 08/20] Update Lib/idlelib/editor.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/idlelib/editor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 74ad176ed47831..1d0f0d3d048b12 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1379,9 +1379,7 @@ def smart_backspace_event(self, event): have -= 1 if have <= want or chars[i-1] not in " \t": break - # Perform the actual removal chars = chars[:len(chars) - ncharsdeleted] - text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: From 6a59e50c7155ad496d6ca9886b58436984f46e2d Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 11:49:47 -0500 Subject: [PATCH 09/20] rem blank line --- Lib/idlelib/editor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 1d0f0d3d048b12..4ba07bcf8b873c 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1365,7 +1365,6 @@ def smart_backspace_event(self, event): have = len(chars.expandtabs(tabwidth)) assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth - # Debug prompt is multilined.... ncharsdeleted = 0 have = len(chars.expandtabs(tabwidth)) From 546f19bdc2091e30f9670dbd2bec761f50d8c56d Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 13:44:40 -0500 Subject: [PATCH 10/20] write some tests --- Lib/idlelib/editor.py | 31 ++++++++++++++++------------ Lib/idlelib/idle_test/test_editor.py | 11 ++++++++++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 4ba07bcf8b873c..661a2372b2770f 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1338,6 +1338,23 @@ def set_indentation_params(self, is_py_src, guess=True): self.usetabs = False self.set_tk_tabwidth(self.tabwidth) + @staticmethod + def delete_trail_whitespace(want, chars, tabwidth): + ncharsdeleted = 0 + have = len(chars.expandtabs(tabwidth)) + for i in range(len(chars) - 1, -1, -1): + # ``Delete'' chars[i], and subtract count + # (since redoing expandtabs is O(n)) + ncharsdeleted += 1 + if chars[i] == '\t': + have -= tabwidth + else: + have -= 1 + if have <= want or chars[i-1] not in " \t": + break + chars = chars[:len(chars) - ncharsdeleted] + return ncharsdeleted, chars + def smart_backspace_event(self, event): text = self.text first, last = self.get_selection_indices() @@ -1366,19 +1383,7 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... - ncharsdeleted = 0 - have = len(chars.expandtabs(tabwidth)) - for i in range(len(chars) - 1, -1, -1): - # ``Delete'' chars[i], and subtract count - # (since redoing expandtabs is O(n)) - ncharsdeleted += 1 - if chars[i] == '\t': - have -= tabwidth - else: - have -= 1 - if have <= want or chars[i-1] not in " \t": - break - chars = chars[:len(chars) - ncharsdeleted] + ncharsdeleted, chars = TestWindow.delete_trail_whitespace(want, chars, tabwidth) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 0dfe2f3c58befa..fd1b1c9634cad7 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -237,5 +237,16 @@ def test_rclick(self): pass +class DeleteWantTest(unittest.TestCase): + + def test_delete_trail_whitespace(self): + test_str = "abcde" + 10000 * "\t" + 10000 * " " + res_str = Editor.delete_trail_whitespace(30000, test_str, 4)[1] + self.assertEqual(res_str, "abcde" + 7498 * "\t") + res_str = Editor.delete_trail_whitespace(41005, test_str, 4)[1] + self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1000 * " ") + res_str = Editor.delete_trail_whitespace(4, test_str, 4)[1] + self.assertEqual(res_str, "abcd") + if __name__ == '__main__': unittest.main(verbosity=2) From 605373b00b2c0415667a950c594c7404c9c86c1b Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 13:47:22 -0500 Subject: [PATCH 11/20] static --- Lib/idlelib/editor.py | 5 ++--- Lib/idlelib/idle_test/test_editor.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 661a2372b2770f..7a68a8d2e90d22 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1338,8 +1338,7 @@ def set_indentation_params(self, is_py_src, guess=True): self.usetabs = False self.set_tk_tabwidth(self.tabwidth) - @staticmethod - def delete_trail_whitespace(want, chars, tabwidth): + def delete_trail_whitespace(self, want, chars, tabwidth): ncharsdeleted = 0 have = len(chars.expandtabs(tabwidth)) for i in range(len(chars) - 1, -1, -1): @@ -1383,7 +1382,7 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... - ncharsdeleted, chars = TestWindow.delete_trail_whitespace(want, chars, tabwidth) + ncharsdeleted, chars = self.delete_trail_whitespace(want, chars, tabwidth) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index fd1b1c9634cad7..e50e5b7154f2ec 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -240,13 +240,15 @@ def test_rclick(self): class DeleteWantTest(unittest.TestCase): def test_delete_trail_whitespace(self): - test_str = "abcde" + 10000 * "\t" + 10000 * " " - res_str = Editor.delete_trail_whitespace(30000, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 7498 * "\t") - res_str = Editor.delete_trail_whitespace(41005, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1000 * " ") - res_str = Editor.delete_trail_whitespace(4, test_str, 4)[1] - self.assertEqual(res_str, "abcd") + with unittest.mock.patch.object(Editor, '__init__', return_value=None) as mock_init: + ew = Editor() + test_str = "abcde" + 10000 * "\t" + 10000 * " " + res_str = ew.delete_trail_whitespace(30000, test_str, 4)[1] + self.assertEqual(res_str, "abcde" + 7498 * "\t") + res_str = ew.delete_trail_whitespace(41005, test_str, 4)[1] + self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1000 * " ") + res_str = ew.delete_trail_whitespace(4, test_str, 4)[1] + self.assertEqual(res_str, "abcd") if __name__ == '__main__': unittest.main(verbosity=2) From 9625fccc9203ff2c4f4eae23cf5f39f0ff2eada2 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 13:47:55 -0500 Subject: [PATCH 12/20] linter --- Lib/idlelib/idle_test/test_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index e50e5b7154f2ec..4ed45a16830b13 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -238,7 +238,7 @@ def test_rclick(self): class DeleteWantTest(unittest.TestCase): - + def test_delete_trail_whitespace(self): with unittest.mock.patch.object(Editor, '__init__', return_value=None) as mock_init: ew = Editor() From d0017b6ede9fadb0904680b96e35f1c1b03b1a91 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 14:27:28 -0500 Subject: [PATCH 13/20] get this right --- Lib/idlelib/editor.py | 26 ++++++++++++++------------ Lib/idlelib/idle_test/test_editor.py | 8 ++++---- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 7a68a8d2e90d22..270af0172e8f3a 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1339,20 +1339,22 @@ def set_indentation_params(self, is_py_src, guess=True): self.set_tk_tabwidth(self.tabwidth) def delete_trail_whitespace(self, want, chars, tabwidth): - ncharsdeleted = 0 - have = len(chars.expandtabs(tabwidth)) - for i in range(len(chars) - 1, -1, -1): - # ``Delete'' chars[i], and subtract count - # (since redoing expandtabs is O(n)) - ncharsdeleted += 1 - if chars[i] == '\t': - have -= tabwidth + current_pos = 0 + ncharsretained = 0 + for char in chars: + if char == '\t': + current_pos = (current_pos // tabwidth + 1) * tabwidth else: - have -= 1 - if have <= want or chars[i-1] not in " \t": + current_pos += 1 + ncharsretained += 1 + if current_pos > want: + ncharsretained -= 1 break - chars = chars[:len(chars) - ncharsdeleted] - return ncharsdeleted, chars + for i in range(ncharsretained, len(chars)): + if chars[i] not in " \t": + ncharsretained = i + 1 + chars = chars[:ncharsretained] + return len(chars) - ncharsretained, chars def smart_backspace_event(self, event): text = self.text diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 4ed45a16830b13..b3652740444e2e 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -244,11 +244,11 @@ def test_delete_trail_whitespace(self): ew = Editor() test_str = "abcde" + 10000 * "\t" + 10000 * " " res_str = ew.delete_trail_whitespace(30000, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 7498 * "\t") + self.assertEqual(res_str, "abcde" + 7499 * "\t") res_str = ew.delete_trail_whitespace(41005, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1000 * " ") - res_str = ew.delete_trail_whitespace(4, test_str, 4)[1] - self.assertEqual(res_str, "abcd") + self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1001 * " ") + res_str = ew.delete_trail_whitespace(3, test_str, 4)[1] + self.assertEqual(res_str, "abcde") if __name__ == '__main__': unittest.main(verbosity=2) From 88c318c33dd001e35d00d662669bb8d4ebe7f08d Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 14:43:03 -0500 Subject: [PATCH 14/20] write more tests --- Lib/idlelib/idle_test/test_editor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index b3652740444e2e..5c3dac0f2aaa06 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -249,6 +249,10 @@ def test_delete_trail_whitespace(self): self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1001 * " ") res_str = ew.delete_trail_whitespace(3, test_str, 4)[1] self.assertEqual(res_str, "abcde") + res_str = ew.delete_trail_whitespace(6, test_str, 4)[1] + self.assertEqual(res_str, "abcde") + res_str = ew.delete_trail_whitespace(30002, test_str, 4)[1] + self.assertEqual(res_str, "abcde" + 7499 * "\t") if __name__ == '__main__': unittest.main(verbosity=2) From 6323ab86d7c6f50ce9e205d30c768e8c6b1dc79a Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 15:12:33 -0500 Subject: [PATCH 15/20] unconditionally remove last character, tests --- Lib/idlelib/editor.py | 10 ++++----- Lib/idlelib/idle_test/test_editor.py | 33 +++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 270af0172e8f3a..87f80f40285f95 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1338,7 +1338,8 @@ def set_indentation_params(self, is_py_src, guess=True): self.usetabs = False self.set_tk_tabwidth(self.tabwidth) - def delete_trail_whitespace(self, want, chars, tabwidth): + def delete_trail_char_and_space(self, want, chars, tabwidth): + chars = chars[:-1] # remove last character unconditionally current_pos = 0 ncharsretained = 0 for char in chars: @@ -1346,15 +1347,14 @@ def delete_trail_whitespace(self, want, chars, tabwidth): current_pos = (current_pos // tabwidth + 1) * tabwidth else: current_pos += 1 - ncharsretained += 1 if current_pos > want: - ncharsretained -= 1 break + ncharsretained += 1 for i in range(ncharsretained, len(chars)): if chars[i] not in " \t": ncharsretained = i + 1 chars = chars[:ncharsretained] - return len(chars) - ncharsretained, chars + return len(chars) - ncharsretained + 1, chars # removal of last def smart_backspace_event(self, event): text = self.text @@ -1384,7 +1384,7 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... - ncharsdeleted, chars = self.delete_trail_whitespace(want, chars, tabwidth) + ncharsdeleted, chars = self.delete_trail_char_and_space(want, chars, tabwidth) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 5c3dac0f2aaa06..8f8d7ddd2e5d2d 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -239,20 +239,41 @@ def test_rclick(self): class DeleteWantTest(unittest.TestCase): - def test_delete_trail_whitespace(self): + def test_delete_trail_char_and_space(self): with unittest.mock.patch.object(Editor, '__init__', return_value=None) as mock_init: ew = Editor() + test_str = "abcde" + 10000 * "\t" + 10000 * " " - res_str = ew.delete_trail_whitespace(30000, test_str, 4)[1] + res_str = ew.delete_trail_char_and_space(30000, test_str, 4)[1] self.assertEqual(res_str, "abcde" + 7499 * "\t") - res_str = ew.delete_trail_whitespace(41005, test_str, 4)[1] + res_str = ew.delete_trail_char_and_space(41005, test_str, 4)[1] self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1001 * " ") - res_str = ew.delete_trail_whitespace(3, test_str, 4)[1] + res_str = ew.delete_trail_char_and_space(3, test_str, 4)[1] self.assertEqual(res_str, "abcde") - res_str = ew.delete_trail_whitespace(6, test_str, 4)[1] + res_str = ew.delete_trail_char_and_space(6, test_str, 4)[1] self.assertEqual(res_str, "abcde") - res_str = ew.delete_trail_whitespace(30002, test_str, 4)[1] + res_str = ew.delete_trail_char_and_space(30002, test_str, 4)[1] self.assertEqual(res_str, "abcde" + 7499 * "\t") + + test_str = "abcde\tabd\t\t" + res_str = ew.delete_trail_char_and_space(7, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd") + res_str = ew.delete_trail_char_and_space(12, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t") + res_str = ew.delete_trail_char_and_space(13, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t") + res_str = ew.delete_trail_char_and_space(16, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t") + + test_str = "abcde\tabd\t \ta" + res_str = ew.delete_trail_char_and_space(7, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd") + res_str = ew.delete_trail_char_and_space(12, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t") + res_str = ew.delete_trail_char_and_space(13, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t ") + res_str = ew.delete_trail_char_and_space(16, test_str, 4)[1] + self.assertEqual(res_str, "abcde\tabd\t \t") if __name__ == '__main__': unittest.main(verbosity=2) From 01921fc070b43af2959f3168c1329783989450f0 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 15:34:38 -0500 Subject: [PATCH 16/20] more organized testing also speedup test --- Lib/idlelib/idle_test/test_editor.py | 86 +++++++++++++++++----------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 8f8d7ddd2e5d2d..061ba523553ad9 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -5,6 +5,7 @@ from collections import namedtuple from test.support import requires from tkinter import Tk, Text +import time Editor = editor.EditorWindow @@ -239,41 +240,62 @@ def test_rclick(self): class DeleteWantTest(unittest.TestCase): + data = [ + ("abcde" + 10000 * "\t" + 10000 * " ", [ + (30000, 4, "abcde" + 7499 * "\t"), + (41005, 4, "abcde" + 10000 * "\t" + 1001 * " "), + (3, 4, "abcde"), + (6, 4, "abcde"), + (30002, 4, "abcde" + 7499 * "\t"), + ]), + ("abcde\tabd\t\t", [ + (7, 4, "abcde\tabd"), + (12, 4, "abcde\tabd\t"), + (13, 4, "abcde\tabd\t"), + (16, 4, "abcde\tabd\t"), + ]), + ("abcde\tabd\t \ta", [ + (7, 4, "abcde\tabd"), + (12, 4, "abcde\tabd\t"), + (13, 4, "abcde\tabd\t "), + (16, 4, "abcde\tabd\t \t"), + ]), + ] + + def mock_delete_trail_char_and_space(self, want, chars, tabwidth): + ncharsdeleted = 0 + while True: + chars = chars[:-1] + ncharsdeleted = ncharsdeleted + 1 + have = len(chars.expandtabs(tabwidth)) + if have <= want or chars[-1] not in " \t": + break + return ncharsdeleted, chars + def test_delete_trail_char_and_space(self): with unittest.mock.patch.object(Editor, '__init__', return_value=None) as mock_init: + initial_time_new = time.time() ew = Editor() - - test_str = "abcde" + 10000 * "\t" + 10000 * " " - res_str = ew.delete_trail_char_and_space(30000, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 7499 * "\t") - res_str = ew.delete_trail_char_and_space(41005, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 10000 * "\t" + 1001 * " ") - res_str = ew.delete_trail_char_and_space(3, test_str, 4)[1] - self.assertEqual(res_str, "abcde") - res_str = ew.delete_trail_char_and_space(6, test_str, 4)[1] - self.assertEqual(res_str, "abcde") - res_str = ew.delete_trail_char_and_space(30002, test_str, 4)[1] - self.assertEqual(res_str, "abcde" + 7499 * "\t") - - test_str = "abcde\tabd\t\t" - res_str = ew.delete_trail_char_and_space(7, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd") - res_str = ew.delete_trail_char_and_space(12, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t") - res_str = ew.delete_trail_char_and_space(13, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t") - res_str = ew.delete_trail_char_and_space(16, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t") - - test_str = "abcde\tabd\t \ta" - res_str = ew.delete_trail_char_and_space(7, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd") - res_str = ew.delete_trail_char_and_space(12, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t") - res_str = ew.delete_trail_char_and_space(13, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t ") - res_str = ew.delete_trail_char_and_space(16, test_str, 4)[1] - self.assertEqual(res_str, "abcde\tabd\t \t") + for dat in self.data: + test_str = dat[0] + for da in dat[1]: + with self.subTest(want=da[0], tabwidth=da[1], input=repr(test_str)): + res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] + self.assertEqual(res_str, da[2]) + time_new = time.time() - initial_time_new + + initial_time_old = time.time() + with unittest.mock.patch.object(Editor, 'delete_trail_char_and_space', self.mock_delete_trail_char_and_space): + ew = Editor() + for dat in self.data: + test_str = dat[0] + for da in dat[1]: + with self.subTest(want=da[0], tabwidth=da[1], input=repr(test_str)): + res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] + self.assertEqual(res_str, da[2]) + time_old = time.time() - initial_time_old + + self.assertGreaterEqual(time_old / time_new, 10) if __name__ == '__main__': unittest.main(verbosity=2) From 5f9bb55e3d820176bc6bbd4474fdee726d971cc3 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 16:43:48 -0500 Subject: [PATCH 17/20] grumble --- Lib/idlelib/editor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 87f80f40285f95..0e104091305511 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1385,6 +1385,7 @@ def smart_backspace_event(self, event): want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... ncharsdeleted, chars = self.delete_trail_char_and_space(want, chars, tabwidth) + have = len(chars.expandtabs(tabwidth)) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: From a087395343b8938c00b6a459ac84a5f32d9d7d85 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 17:47:07 -0500 Subject: [PATCH 18/20] fix some tests and add some new ones --- Lib/idlelib/idle_test/test_editor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 061ba523553ad9..595b1e9ba91d3a 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -1,4 +1,4 @@ -"Test editor, coverage 53%." +"Test editor, coverage approximately 53%." from idlelib import editor import unittest @@ -260,6 +260,10 @@ class DeleteWantTest(unittest.TestCase): (13, 4, "abcde\tabd\t "), (16, 4, "abcde\tabd\t \t"), ]), + ("\tabcd", [ + (2, 4, ""), + (5, 4, "\ta"), + ]), ] def mock_delete_trail_char_and_space(self, want, chars, tabwidth): @@ -279,7 +283,7 @@ def test_delete_trail_char_and_space(self): for dat in self.data: test_str = dat[0] for da in dat[1]: - with self.subTest(want=da[0], tabwidth=da[1], input=repr(test_str)): + with self.subTest(want=da[0], tabwidth=da[1], input=test_str): res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] self.assertEqual(res_str, da[2]) time_new = time.time() - initial_time_new @@ -290,7 +294,7 @@ def test_delete_trail_char_and_space(self): for dat in self.data: test_str = dat[0] for da in dat[1]: - with self.subTest(want=da[0], tabwidth=da[1], input=repr(test_str)): + with self.subTest(want=da[0], tabwidth=da[1], input=test_str): res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] self.assertEqual(res_str, da[2]) time_old = time.time() - initial_time_old From 6604a73df52e113df95531757ca946dc08504649 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 18:19:31 -0500 Subject: [PATCH 19/20] fix everythign --- Lib/idlelib/editor.py | 6 +++-- Lib/idlelib/idle_test/test_editor.py | 40 +++++++++++++--------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 0e104091305511..2327cfbf9b8b77 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1354,7 +1354,7 @@ def delete_trail_char_and_space(self, want, chars, tabwidth): if chars[i] not in " \t": ncharsretained = i + 1 chars = chars[:ncharsretained] - return len(chars) - ncharsretained + 1, chars # removal of last + return chars def smart_backspace_event(self, event): text = self.text @@ -1384,7 +1384,9 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... - ncharsdeleted, chars = self.delete_trail_char_and_space(want, chars, tabwidth) + oldchars = chars + chars = self.delete_trail_char_and_space(want, chars, tabwidth) + ncharsdeleted = len(oldchars) - len(chars) have = len(chars.expandtabs(tabwidth)) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 595b1e9ba91d3a..1b4575312635db 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -260,9 +260,8 @@ class DeleteWantTest(unittest.TestCase): (13, 4, "abcde\tabd\t "), (16, 4, "abcde\tabd\t \t"), ]), - ("\tabcd", [ - (2, 4, ""), - (5, 4, "\ta"), + (" ", [ + (2, 2, " "), ]), ] @@ -274,30 +273,27 @@ def mock_delete_trail_char_and_space(self, want, chars, tabwidth): have = len(chars.expandtabs(tabwidth)) if have <= want or chars[-1] not in " \t": break - return ncharsdeleted, chars + return chars + + def do_tests(self): + ew = Editor() + for dat in self.data: + test_str = dat[0] + for da in dat[1]: + with self.subTest(want=da[0], tabwidth=da[1], input=test_str): + res = ew.delete_trail_char_and_space(da[0], test_str, da[1]) + self.assertEqual(res, da[2]) def test_delete_trail_char_and_space(self): - with unittest.mock.patch.object(Editor, '__init__', return_value=None) as mock_init: + with unittest.mock.patch.object(Editor, '__init__', return_value=None): initial_time_new = time.time() - ew = Editor() - for dat in self.data: - test_str = dat[0] - for da in dat[1]: - with self.subTest(want=da[0], tabwidth=da[1], input=test_str): - res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] - self.assertEqual(res_str, da[2]) + self.do_tests() time_new = time.time() - initial_time_new - - initial_time_old = time.time() + with unittest.mock.patch.object(Editor, 'delete_trail_char_and_space', self.mock_delete_trail_char_and_space): - ew = Editor() - for dat in self.data: - test_str = dat[0] - for da in dat[1]: - with self.subTest(want=da[0], tabwidth=da[1], input=test_str): - res_str = ew.delete_trail_char_and_space(da[0], test_str, da[1])[1] - self.assertEqual(res_str, da[2]) - time_old = time.time() - initial_time_old + initial_time_old = time.time() + self.do_tests() + time_old = time.time() - initial_time_old self.assertGreaterEqual(time_old / time_new, 10) From 3f0abfb74605ad6fef8e4542f9c199d5ac6ca1c1 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Sun, 1 Jun 2025 18:32:31 -0500 Subject: [PATCH 20/20] precommit --- Lib/idlelib/idle_test/test_editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 1b4575312635db..b5ecf36d2eb345 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -274,7 +274,7 @@ def mock_delete_trail_char_and_space(self, want, chars, tabwidth): if have <= want or chars[-1] not in " \t": break return chars - + def do_tests(self): ew = Editor() for dat in self.data: @@ -289,7 +289,7 @@ def test_delete_trail_char_and_space(self): initial_time_new = time.time() self.do_tests() time_new = time.time() - initial_time_new - + with unittest.mock.patch.object(Editor, 'delete_trail_char_and_space', self.mock_delete_trail_char_and_space): initial_time_old = time.time() self.do_tests()