diff --git a/Lib/curses/textpad.py b/Lib/curses/textpad.py index 2b4b4cb669f..2079953a066 100644 --- a/Lib/curses/textpad.py +++ b/Lib/curses/textpad.py @@ -43,16 +43,20 @@ class Textbox: def __init__(self, win, insert_mode=False): self.win = win self.insert_mode = insert_mode - (self.maxy, self.maxx) = win.getmaxyx() - self.maxy = self.maxy - 1 - self.maxx = self.maxx - 1 + self._update_max_yx() self.stripspaces = 1 self.lastcmd = None win.keypad(1) + def _update_max_yx(self): + maxy, maxx = self.win.getmaxyx() + self.maxy = maxy - 1 + self.maxx = maxx - 1 + def _end_of_line(self, y): """Go to the location of the first blank on the given line, returning the index of the last non-blank character.""" + self._update_max_yx() last = self.maxx while True: if curses.ascii.ascii(self.win.inch(y, last)) != curses.ascii.SP: @@ -64,8 +68,10 @@ class Textbox: return last def _insert_printable_char(self, ch): + self._update_max_yx() (y, x) = self.win.getyx() - if y < self.maxy or x < self.maxx: + backyx = None + while y < self.maxy or x < self.maxx: if self.insert_mode: oldch = self.win.inch() # The try-catch ignores the error we trigger from some curses @@ -75,14 +81,20 @@ class Textbox: self.win.addch(ch) except curses.error: pass - if self.insert_mode: - (backy, backx) = self.win.getyx() - if curses.ascii.isprint(oldch): - self._insert_printable_char(oldch) - self.win.move(backy, backx) + if not self.insert_mode or not curses.ascii.isprint(oldch): + break + ch = oldch + (y, x) = self.win.getyx() + # Remember where to put the cursor back since we are in insert_mode + if backyx is None: + backyx = y, x + + if backyx is not None: + self.win.move(*backyx) def do_command(self, ch): "Process a single editing command." + self._update_max_yx() (y, x) = self.win.getyx() self.lastcmd = ch if curses.ascii.isprint(ch): @@ -148,6 +160,7 @@ class Textbox: def gather(self): "Collect and return the contents of the window." result = "" + self._update_max_yx() for y in range(self.maxy+1): self.win.move(y, 0) stop = self._end_of_line(y) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 25284ad522b..14ba87f8caa 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -27,6 +27,7 @@ requires('curses') curses = import_module('curses') import_module('curses.panel') import_module('curses.ascii') +import_module('curses.textpad') def requires_curses_func(name): return unittest.skipUnless(hasattr(curses, name), @@ -392,6 +393,14 @@ class TestCurses(unittest.TestCase): human_readable_signature = stdscr.addch.__doc__.split("\n")[0] self.assertIn("[y, x,]", human_readable_signature) + def test_issue13051(self): + stdscr = self.stdscr + box = curses.textpad.Textbox(stdscr, insert_mode=True) + lines, cols = stdscr.getmaxyx() + stdscr.resize(lines-2, cols-2) + # this may cause infinite recursion, leading to a RuntimeError + box._insert_printable_char('a') + class MiscTests(unittest.TestCase): diff --git a/Misc/ACKS b/Misc/ACKS index 66c36608af7..35b26113acc 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -41,6 +41,7 @@ A. Amoroso Mark Anacker Shashwat Anand Anders Andersen +Tycho Andersen John Anderson Pehr Anderson Erik Andersén diff --git a/Misc/NEWS b/Misc/NEWS index 9c972b1e32f..11ec5f5811e 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -140,6 +140,9 @@ Core and Builtins Library ------- +- Issue #13051: Fixed recursion errors in large or resized + curses.textpad.Textbox. Based on patch by Tycho Andersen. + - Issue #9770: curses.ascii predicates now work correctly with negative integers.