diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index d2f11949c04c3f..8987e82ee5d026 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -206,17 +206,15 @@ The module :mod:`!curses` defines the following functions: .. function:: erasechar() - Return the user's current erase character as a one-byte bytes object. Under Unix operating systems this - is a property of the controlling tty of the curses program, and is not set by - the curses library itself. + Return the user's current erase character as a raw byte, a :class:`bytes` + object of length 1. See also :func:`erasewchar`. .. function:: erasewchar() - Return the user's current erase character as a one-character string. - This is the wide-character variant of :func:`erasechar`. Availability - depends on building Python against a wide-character-aware version of the - underlying curses library. + Return the user's current erase character as a one-character :class:`str`. + Under Unix operating systems this is a property of the controlling tty of the + curses program, and is not set by the curses library itself. .. versionadded:: next @@ -493,17 +491,15 @@ The module :mod:`!curses` defines the following functions: .. function:: killchar() - Return the user's current line kill character as a one-byte bytes object. Under Unix operating systems - this is a property of the controlling tty of the curses program, and is not set - by the curses library itself. + Return the user's current line kill character as a raw byte, a :class:`bytes` + object of length 1. See also :func:`killwchar`. .. function:: killwchar() - Return the user's current line kill character as a one-character string. - This is the wide-character variant of :func:`killchar`. Availability - depends on building Python against a wide-character-aware version of the - underlying curses library. + Return the user's current line kill character as a one-character :class:`str`. + Under Unix operating systems this is a property of the controlling tty of the + curses program, and is not set by the curses library itself. .. versionadded:: next @@ -910,30 +906,39 @@ The module :mod:`!curses` defines the following functions: .. function:: unctrl(ch) Return a bytes object which is a printable representation of the character *ch*. - Control characters are represented as a caret followed by the character, for - example as ``b'^C'``. Printing characters are left as they are. + *ch* cannot be a character that does not fit in a single byte; use + :func:`wunctrl` for those. .. function:: wunctrl(ch) - Return a string which is a printable representation of the wide character *ch*. - Control characters are represented as a caret followed by the character, for - example as ``'^C'``. Printing characters are left as they are. This is the - wide-character variant of :func:`unctrl`, returning a :class:`str` rather than - :class:`bytes`. Availability depends on building Python against a - wide-character-aware version of the underlying curses library. + Return a string which is a printable representation of the character *ch*; + any attributes and color pair are ignored. + ASCII control characters are represented as a caret followed by a character, + for example as ``'^C'``. Printing characters, including non-ASCII characters + printable in the locale, are left as they are. The representation of other + characters is defined by the underlying curses library. .. versionadded:: next .. function:: ungetch(ch) - Push *ch* so the next :meth:`~window.getch` will return it. + Push *ch* so the next :meth:`~window.getch` or :meth:`~window.get_wch` will + return it. + + *ch* may be an integer (a key code or character code), a byte, or a string of + length 1. A one-character string is pushed like :func:`unget_wch`; on a + narrow build it must encode to a single byte. .. note:: Only one *ch* can be pushed before :meth:`!getch` is called. + .. versionchanged:: next + A one-character string argument is no longer required to encode to a single + byte, except on a narrow build. + .. function:: update_lines_cols() @@ -953,6 +958,10 @@ The module :mod:`!curses` defines the following functions: .. versionadded:: 3.3 + .. versionchanged:: next + Also available on a narrow build, where *ch* must encode to a single byte + (an 8-bit locale). + .. function:: ungetmouse(id, x, y, z, bstate) @@ -1323,15 +1332,16 @@ Window objects .. method:: window.getbkgd() Return the given window's current background character/attribute pair. + It cannot represent a background set with a wide character or with a color + pair outside the :func:`color_pair` range; use :meth:`getbkgrnd` for those. .. method:: window.getbkgrnd() Return the given window's current background as a :class:`complexchar`. - This is the wide-character variant of :meth:`getbkgd`: the returned object - carries the background character together with its attributes and color pair, - and the color pair is not limited to the value that fits in a - :func:`color_pair`. + Unlike :meth:`getbkgd`, the returned object carries the background character + together with its attributes and color pair, and the color pair is not limited + to the value that fits in a :func:`color_pair`. .. versionadded:: next @@ -1342,16 +1352,23 @@ Window objects range: function keys, keypad keys and so on are represented by numbers higher than 255. In no-delay mode, return ``-1`` if there is no input, otherwise wait until a key is pressed. + A multibyte character is returned as its encoded bytes one at a time; use + :meth:`get_wch` to read it as a single character. .. method:: window.get_wch([y, x]) Get a wide character. Return a character for most keys, or an integer for - function keys, keypad keys, and other special keys. + function keys, keypad keys, and other special keys. Unlike :meth:`getch`, an + ordinary key is returned as a one-character :class:`str`. In no-delay mode, raise an exception if there is no input. .. versionadded:: 3.3 + .. versionchanged:: next + Also available on a narrow build, where only a character representable as a + single byte (an 8-bit locale) can be returned. + .. method:: window.getdelay() @@ -1407,6 +1424,8 @@ Window objects Read a bytes object from the user, with primitive line editing capacity. At most *n* characters are read; *n* defaults to and cannot exceed 2047. + A multibyte character is returned as its encoded bytes; use :meth:`get_wstr` + to read the input as a :class:`str`. .. versionchanged:: 3.14 The maximum value for *n* was increased from 1023 to 2047. @@ -1418,9 +1437,8 @@ Window objects window.get_wstr(y, x, n) Read a string from the user, with primitive line editing capacity. - This is the wide-character variant of :meth:`getstr`: it returns a - :class:`str` rather than a :class:`bytes` object, so it can return - characters that are not representable in the window's encoding. + Unlike :meth:`getstr`, it can return characters that are not representable in + the window's encoding. At most *n* characters are read; *n* defaults to and cannot exceed 2047. .. versionadded:: next @@ -1470,15 +1488,17 @@ Window objects Return the character at the given position in the window. The bottom 8 bits are the character proper, and upper bits are the attributes. + It cannot represent a cell holding combining characters, a character that does + not fit in a single byte, or a color pair outside the :func:`color_pair` + range; use :meth:`in_wch` for those. .. method:: window.in_wch([y, x]) Return the complex character at the given position in the window as a - :class:`complexchar`. This is the wide-character variant of :meth:`inch`: - the returned object carries the cell's text (a spacing character optionally - followed by combining characters) together with its attributes and color - pair, none of which :meth:`inch` can represent. + :class:`complexchar`. Unlike :meth:`inch`, the returned object carries the + cell's text (a spacing character optionally followed by combining characters) + together with its attributes and color pair. .. versionadded:: next @@ -1548,6 +1568,8 @@ Window objects from the characters. If *n* is specified, :meth:`instr` returns a string at most *n* characters long (exclusive of the trailing NUL). The maximum value for *n* is 2047. + A character not representable in the window's encoding cannot be returned; + use :meth:`in_wstr` for those. .. versionchanged:: 3.14 The maximum value for *n* was increased from 1023 to 2047. @@ -1557,11 +1579,10 @@ Window objects window.in_wstr(y, x[, n]) Return a string of characters, extracted from the window starting at the - current cursor position, or at *y*, *x* if specified. This is the - wide-character variant of :meth:`instr`: it returns a :class:`str` rather - than a :class:`bytes` object, so it can return characters that are not - representable in the window's encoding. Attributes and color information - are stripped from the characters. The maximum value for *n* is 2047. + current cursor position, or at *y*, *x* if specified. Unlike :meth:`instr`, + it can return characters that are not representable in the window's encoding. + Attributes and color information are stripped from the characters. The + maximum value for *n* is 2047. .. versionadded:: next diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index a87d708e3fcdc2..cbe0df1c8a65ff 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -113,12 +113,20 @@ curses :func:`curses.erasechar`, :func:`curses.killchar` and :func:`curses.unctrl`. On a narrow (non-ncursesw) build the character cell holds a single character without combining marks, representable as one byte in the window's encoding, - and :meth:`~curses.window.in_wstr` returns its decoded text; - :meth:`~curses.window.get_wstr` and the :func:`curses.erasewchar`, - :func:`curses.killwchar` and :func:`curses.wunctrl` functions require the - wide-character ncursesw library. + and :meth:`~curses.window.in_wstr` returns its decoded text. (Contributed by Serhiy Storchaka in :gh:`151757`.) +* The wide-character :mod:`curses` functions and methods + :meth:`~curses.window.get_wch`, :meth:`~curses.window.get_wstr`, + :func:`curses.unget_wch`, :func:`curses.erasewchar`, + :func:`curses.killwchar` and :func:`curses.wunctrl` now also work when Python + is not built against a wide-character-aware curses library, on an 8-bit + locale, where each character is a single byte in the relevant encoding. + :func:`curses.ungetch` now also accepts a one-character string, like + :func:`curses.unget_wch`; on a wide-character build it can be any character + (previously a multibyte character raised :exc:`OverflowError`). + (Contributed by Serhiy Storchaka in :gh:`152470`.) + * Add :func:`curses.nofilter`, which undoes the effect of :func:`curses.filter`. (Contributed by Serhiy Storchaka in :gh:`151744`.) diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index 7611771aa83e0c..079e69a52c1504 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -42,6 +42,31 @@ def wrapped(self, *args, **kwargs): return wrapped return deco +def _wide_build(): + # True on a build that stores wide-character cells (built against ncursesw). + # A wide build accepts a spacing character plus a combining mark in a single + # cell; a narrow build accepts only one character per cell. This stays a + # reliable wide/narrow signal even as the wide-character functions (get_wch() + # and friends) become available on narrow builds too, because the + # multi-codepoint cell capacity itself is build-specific. + if not hasattr(curses, 'complexchar'): + return hasattr(curses.window, 'get_wch') + try: + curses.complexchar('e\u0301') # 'e' + combining acute: two code points + except ValueError: + return False + return True + +WIDE_BUILD = _wide_build() + +def requires_wide_build(test): + @functools.wraps(test) + def wrapped(self, *args, **kwargs): + if not WIDE_BUILD: + raise unittest.SkipTest('requires a wide-character curses build') + test(self, *args, **kwargs) + return wrapped + def requires_colors(test): @functools.wraps(test) @@ -332,7 +357,7 @@ def _storable(self, s): # one byte. if not self._encodable(s): return False - if hasattr(self.stdscr, 'get_wch'): # wide build + if WIDE_BUILD: return True return len(s.encode(self.stdscr.encoding)) == len(s) @@ -347,7 +372,7 @@ def _read_char(self, y, x): return str(stdscr.in_wch(y, x)) return stdscr.instr(y, x, 1).decode(stdscr.encoding) - @requires_curses_window_meth('get_wch') + @requires_wide_build def test_addch_combining(self): stdscr = self.stdscr stdscr.move(0, 0) @@ -368,7 +393,7 @@ def test_addch_combining(self): self.assertRaises(ValueError, stdscr.addch, '\n\u0301') self.assertRaises(ValueError, stdscr.addch, '\ne\u0301') - @requires_curses_window_meth('get_wch') + @requires_wide_build def test_addch_emoji(self): # curses has no grapheme-cluster support: a cell holds one spacing # character plus zero-width combining characters. A lone emoji fits, @@ -385,7 +410,7 @@ def test_addch_emoji(self): self.assertRaises(ValueError, stdscr.addch, '\U0001f468\u200d\U0001f469') # man ZWJ woman - @requires_curses_window_meth('get_wch') + @requires_wide_build def test_wide_characters(self): # Wide and combining characters in the character-cell methods. stdscr = self.stdscr @@ -407,7 +432,6 @@ def test_wide_characters(self): # border() and box() cannot mix integer and wide-string characters. self.assertRaises(TypeError, stdscr.box, vline, ord('-')) - @requires_curses_func('complexchar') def test_complexchar_in_cell_methods(self): # Every single-character-cell method also accepts a complexchar, whose # attributes and color pair come from the cell itself. @@ -438,7 +462,6 @@ def test_complexchar_in_cell_methods(self): self.assertRaises(TypeError, stdscr.hline, h, 3, curses.A_BOLD) self.assertRaises(TypeError, stdscr.vline, v, 3, curses.A_BOLD) - @requires_curses_window_meth('in_wstr') def test_in_wstr(self): # The wide-character window read returns a str (instr returns bytes). # See _encodable for the character set. @@ -455,7 +478,6 @@ def test_in_wstr(self): self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes) - @requires_curses_func('complexchar') def test_complexchar(self): # A complexchar is a styled wide-character cell: str() is its text, # and the attr and pair attributes are its rendition. @@ -499,7 +521,6 @@ def test_complexchar(self): self.assertRaises(ValueError, curses.complexchar, 'A', 0, -1) self.assertRaises(ValueError, curses.complexchar, 'ab') - @requires_curses_window_meth('in_wch') def test_in_wch(self): # in_wch() returns the styled wide cell as a complexchar -- something # inch() (a packed chtype) cannot represent. @@ -518,7 +539,6 @@ def test_in_wch(self): stdscr.move(0, 0) self.assertEqual(str(stdscr.in_wch()), 'A') - @requires_curses_window_meth('in_wch') @requires_colors def test_in_wch_color(self): # Unlike the chtype methods (which pack the pair into the value via @@ -532,7 +552,6 @@ def test_in_wch_color(self): self.assertEqual(cc.pair, 1) self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) - @requires_curses_window_meth('getbkgrnd') def test_getbkgrnd(self): # getbkgrnd() returns the background as a complexchar (getbkgd() can # only return a packed chtype). @@ -550,7 +569,6 @@ def test_getbkgrnd(self): self.assertEqual(str(stdscr.getbkgrnd()), ch) stdscr.bkgd(' ') - @requires_curses_func('complexstr') def test_complexstr(self): # A complexstr is an immutable run of styled wide-character cells: the # string counterpart of complexchar (as str is to a single character). @@ -602,7 +620,7 @@ def test_complexstr(self): base = 'é' # 'e' + combining acute: two code points, one cell # Combining sequences need wide-character cells (a narrow build stores # one byte per cell). - if hasattr(curses.window, 'get_wch') and self._encodable(base): + if WIDE_BUILD and self._encodable(base): self.assertEqual(len(curses.complexstr(base)), 1) self.assertEqual(curses.complexstr(base)[0], cc(base)) self.assertEqual(len(curses.complexstr('a' + base + 'b')), 3) @@ -635,7 +653,6 @@ def test_complexstr(self): self.assertRaises(TypeError, lambda: curses.complexstr(['A'], pair=0)) - @requires_curses_window_meth('in_wchstr') def test_in_wchstr(self): # in_wchstr() returns a complexstr -- the styled-cell counterpart of # instr() (bytes) and in_wstr() (str), which both strip the rendition. @@ -655,7 +672,6 @@ def test_in_wchstr(self): stdscr.move(0, 0) self.assertEqual(str(stdscr.in_wchstr())[:3], 'AbC') - @requires_curses_window_meth('in_wchstr') def test_complexstr_in_write_methods(self): # addstr/addnstr/insstr/insnstr also accept a complexstr, written via # the wide-character functions; a plain str keeps its current meaning. @@ -750,11 +766,8 @@ def test_output_character(self): # str is stored as a wide-character cell on a wide build, so every # encodable character round-trips, insch() included. A multibyte # character does not fit a cell on a narrow build and is skipped. - wide = hasattr(stdscr, 'get_wch') for c in ('é', '¤', '€', 'є'): - if not self._encodable(c): - continue - if not wide and len(c.encode(encoding)) != 1: + if not self._storable(c): continue with self.subTest(c=c): stdscr.addch(0, 0, c) @@ -998,7 +1011,6 @@ def test_getstr(self): self.assertEqual(win.getstr(), b'amet') self.assertEqual(win.instr(1, 0), b'amet dolor ') - @requires_curses_window_meth('get_wstr') def test_get_wstr(self): # get_wstr() reads input as a str (getstr() returns bytes); feed it with # unget_wch(). See _encodable for the character set. @@ -1010,7 +1022,7 @@ def test_get_wstr(self): 'naïve ¤', # ISO-8859-1 'soupçon €', # ISO-8859-15 'дяк']: # KOI8-U - if self._encodable(s): + if self._storable(s): with self.subTest(s=s): win.erase() for ch in reversed(s + '\n'): @@ -1465,7 +1477,6 @@ def test_unctrl(self): self.assertRaises(TypeError, curses.unctrl, '') self.assertRaises(TypeError, curses.unctrl, 'AB') - @requires_curses_func('wunctrl') def test_wunctrl(self): # The wide-character variant of unctrl() returns a str. self.assertEqual(curses.wunctrl(b'A'), 'A') @@ -1475,12 +1486,14 @@ def test_wunctrl(self): self.assertEqual(curses.wunctrl(10), '^J') # See _encodable for the character set (all printable here). for c in ('A', 'é', '¤', '€', 'є'): - self.assertEqual(curses.wunctrl(c), c) + if self._storable(c): + self.assertEqual(curses.wunctrl(c), c) self.assertRaises(TypeError, curses.wunctrl, b'') self.assertRaises(TypeError, curses.wunctrl, b'AB') self.assertRaises(TypeError, curses.wunctrl, '') - # More than one spacing character is not a single cell. - self.assertRaises(ValueError, curses.wunctrl, 'AB') + if WIDE_BUILD: + # More than one spacing character is not a single cell. + self.assertRaises(ValueError, curses.wunctrl, 'AB') self.assertRaises(OverflowError, curses.unctrl, 2**64) def test_endwin(self): @@ -1552,14 +1565,12 @@ def test_env_queries(self): tty_fd = None if tty_fd is not None: os.close(tty_fd) - if hasattr(curses, 'erasewchar'): - c = curses.erasewchar() - self.assertIsInstance(c, str) - self.assertEqual(len(c), 1) - if hasattr(curses, 'killwchar'): - c = curses.killwchar() - self.assertIsInstance(c, str) - self.assertEqual(len(c), 1) + c = curses.erasewchar() + self.assertIsInstance(c, str) + self.assertEqual(len(c), 1) + c = curses.killwchar() + self.assertIsInstance(c, str) + self.assertEqual(len(c), 1) @requires_curses_func('define_key') def test_key_management(self): @@ -2434,30 +2445,40 @@ def test_issue6243(self): curses.ungetch(1025) self.stdscr.getkey() - @requires_curses_func('unget_wch') + @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), + "unget_wch is broken in ncurses 5.7 and earlier") + def test_ungetch_wch(self): + # ungetch() also accepts a character, like unget_wch(), and it + # round-trips through get_wch() -- including a character that does not + # fit in a single byte. + stdscr = self.stdscr + for ch in ('a', '\xe9', '\xa4', '€', 'є', '\U0010FFFF'): + if not self._storable(ch): + continue + curses.ungetch(ch) + self.assertEqual(stdscr.get_wch(), ch) + # An int is a raw keycode, not a character codepoint. + curses.ungetch(curses.KEY_LEFT) + self.assertEqual(stdscr.getch(), curses.KEY_LEFT) + @unittest.skipIf(getattr(curses, 'ncurses_version', (99,)) < (5, 8), "unget_wch is broken in ncurses 5.7 and earlier") def test_unget_wch(self): stdscr = self.stdscr encoding = stdscr.encoding - # See _encodable for the character set, plus a non-BMP character. + # See _storable for the character set, plus a non-BMP character. for ch in ('a', '\xe9', '\xa4', '\u20ac', '\u0454', '\U0010FFFF'): - try: - ch.encode(encoding) - except UnicodeEncodeError: + if not self._storable(ch): continue try: curses.unget_wch(ch) except Exception as err: self.fail("unget_wch(%a) failed with encoding %s: %s" - % (ch, stdscr.encoding, err)) - read = stdscr.get_wch() - self.assertEqual(read, ch) - - code = ord(ch) - curses.unget_wch(code) - read = stdscr.get_wch() - self.assertEqual(read, ch) + % (ch, encoding, err)) + self.assertEqual(stdscr.get_wch(), ch) + + curses.unget_wch(ord(ch)) + self.assertEqual(stdscr.get_wch(), ch) def test_encoding(self): stdscr = self.stdscr diff --git a/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst index 421c58298823c0..ee1aa3eff6f0c2 100644 --- a/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst +++ b/Misc/NEWS.d/next/Library/2026-06-20-02-14-55.gh-issue-151757.TP9A2x.rst @@ -6,6 +6,4 @@ the functions :func:`curses.erasewchar`, :func:`curses.killwchar` and :func:`curses.wunctrl`. On a narrow (non-ncursesw) build the character cell holds a single character without combining marks, representable as one byte in the window's encoding, and :meth:`curses.window.in_wstr` returns its decoded -text; :meth:`curses.window.get_wstr` and the :func:`curses.erasewchar`, -:func:`curses.killwchar` and :func:`curses.wunctrl` functions require the -wide-character ncursesw library. +text. diff --git a/Misc/NEWS.d/next/Library/2026-06-28-12-00-00.gh-issue-152470.Wn7Kp3.rst b/Misc/NEWS.d/next/Library/2026-06-28-12-00-00.gh-issue-152470.Wn7Kp3.rst new file mode 100644 index 00000000000000..445e0a1b18f7dd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-28-12-00-00.gh-issue-152470.Wn7Kp3.rst @@ -0,0 +1,9 @@ +The wide-character :mod:`curses` functions and methods +:meth:`curses.window.get_wch`, :meth:`curses.window.get_wstr`, +:func:`curses.unget_wch`, :func:`curses.erasewchar`, :func:`curses.killwchar` +and :func:`curses.wunctrl` now also work when Python is not built against a +wide-character-aware curses library, on an 8-bit locale, where each character +is a single byte in the relevant encoding. :func:`curses.ungetch` now also +accepts a one-character string, like :func:`curses.unget_wch`; on a +wide-character build it can be any character (previously a multibyte character +raised :exc:`OverflowError`). diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 7b9de46cf22ab4..5a0d175b5e997e 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -3238,7 +3238,6 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1, } } -#ifdef HAVE_NCURSESW /*[clinic input] _curses.window.get_wch @@ -3261,6 +3260,7 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, int y, int x) /*[clinic end generated code: output=9f4f86e91fe50ef3 input=dd7e5367fb49dc48]*/ { +#ifdef HAVE_NCURSESW int ct; wint_t rtn; @@ -3282,8 +3282,31 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, return PyLong_FromLong(rtn); else return PyUnicode_FromOrdinal(rtn); -} +#else + /* Without the wide library, read one key with wgetch(): a value above 255 + is a function key (returned as an int); a byte is decoded with the + window's encoding (8-bit locales). */ + int rtn; + Py_BEGIN_ALLOW_THREADS + if (!group_right_1) { + rtn = wgetch(self->win); + } + else { + rtn = mvwgetch(self->win, y, x); + } + Py_END_ALLOW_THREADS + + if (rtn == ERR) { + const char *funcname = group_right_1 ? "mvwgetch" : "wgetch"; + return curses_check_signals_on_input_error(self, funcname, "get_wch"); + } + if (rtn > 255) { + return PyLong_FromLong(rtn); + } + char ch = (char)rtn; + return PyUnicode_Decode(&ch, 1, self->encoding, NULL); #endif +} /* * Helper function for parsing parameters from getstr() and instr(). @@ -3336,16 +3359,17 @@ PyDoc_STRVAR(_curses_window_getstr__doc__, " n\n" " Maximal number of characters."); +/* Read user input into a new bytes object (empty on ERR), with primitive line + editing. Shared by getstr() and, without the wide library, by get_wstr(). */ static PyObject * -PyCursesWindow_getstr(PyObject *op, PyObject *args) +curses_window_getstr_bytes(PyCursesWindowObject *self, PyObject *args, + const char *funcname) { - PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); int rtn, use_xy = 0, y = 0, x = 0; unsigned int max_buf_size = 2048; unsigned int n = max_buf_size - 1; - if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, - "_curses.window.instr")) + if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, funcname)) { return NULL; } @@ -3381,6 +3405,13 @@ PyCursesWindow_getstr(PyObject *op, PyObject *args) return PyBytesWriter_FinishWithSize(writer, strlen(buf)); } +static PyObject * +PyCursesWindow_getstr(PyObject *op, PyObject *args) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + return curses_window_getstr_bytes(self, args, "_curses.window.getstr"); +} + /*[clinic input] _curses.window.hline @@ -3566,16 +3597,18 @@ PyDoc_STRVAR(_curses_window_instr__doc__, "instr() returns a string at most n characters long (exclusive of\n" "the trailing NUL)."); +/* Extract characters from the window into a new bytes object (empty on ERR), + with attributes and color stripped. Shared by instr() and, without the wide + library, by in_wstr(). */ static PyObject * -PyCursesWindow_instr(PyObject *op, PyObject *args) +curses_window_instr_bytes(PyCursesWindowObject *self, PyObject *args, + const char *funcname) { - PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); int rtn, use_xy = 0, y = 0, x = 0; unsigned int max_buf_size = 2048; unsigned int n = max_buf_size - 1; - if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, - "_curses.window.instr")) + if (!curses_clinic_parse_optional_xy_n(args, &y, &x, &n, &use_xy, funcname)) { return NULL; } @@ -3601,7 +3634,13 @@ PyCursesWindow_instr(PyObject *op, PyObject *args) return PyBytesWriter_FinishWithSize(writer, strlen(buf)); } -#ifdef HAVE_NCURSESW +static PyObject * +PyCursesWindow_instr(PyObject *op, PyObject *args) +{ + PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); + return curses_window_instr_bytes(self, args, "_curses.window.instr"); +} + PyDoc_STRVAR(_curses_window_get_wstr__doc__, "get_wstr([[y, x,] n=2047])\n" "Read a string from the user, with primitive line editing capacity.\n" @@ -3619,6 +3658,7 @@ static PyObject * PyCursesWindow_get_wstr(PyObject *op, PyObject *args) { PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); +#ifdef HAVE_NCURSESW int rtn, use_xy = 0, y = 0, x = 0; unsigned int max_buf_size = 2048; unsigned int n = max_buf_size - 1; @@ -3668,8 +3708,21 @@ PyCursesWindow_get_wstr(PyObject *op, PyObject *args) PyMem_Free(wbuf); PyMem_Free(buf); return res; -} +#else + /* Without the wide library, read the bytes as getstr() does and decode them + with the window's encoding. */ + PyObject *bytes = curses_window_getstr_bytes(self, args, + "_curses.window.get_wstr"); + if (bytes == NULL) { + return NULL; + } + PyObject *res = PyUnicode_Decode(PyBytes_AS_STRING(bytes), + PyBytes_GET_SIZE(bytes), + self->encoding, NULL); + Py_DECREF(bytes); + return res; #endif /* HAVE_NCURSESW */ +} PyDoc_STRVAR(_curses_window_in_wstr__doc__, "in_wstr([y, x,] n=2047)\n" @@ -3688,6 +3741,7 @@ static PyObject * PyCursesWindow_in_wstr(PyObject *op, PyObject *args) { PyCursesWindowObject *self = _PyCursesWindowObject_CAST(op); +#ifdef HAVE_NCURSESW int rtn, use_xy = 0, y = 0, x = 0; unsigned int max_buf_size = 2048; unsigned int n = max_buf_size - 1; @@ -3699,7 +3753,6 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) } n = Py_MIN(n, max_buf_size - 1); -#ifdef HAVE_NCURSESW wchar_t *buf = PyMem_New(wchar_t, n + 1); if (buf == NULL) { return PyErr_NoMemory(); @@ -3720,26 +3773,17 @@ PyCursesWindow_in_wstr(PyObject *op, PyObject *args) PyMem_Free(buf); return res; #else - /* Without the wide library, read the locale-encoded bytes and decode them + /* Without the wide library, read the bytes as instr() does and decode them with the window's encoding. */ - char *buf = PyMem_New(char, n + 1); - if (buf == NULL) { - return PyErr_NoMemory(); - } - - if (use_xy) { - rtn = mvwinnstr(self->win, y, x, buf, n); - } - else { - rtn = winnstr(self->win, buf, n); - } - - if (rtn == ERR) { - PyMem_Free(buf); - return Py_GetConstant(Py_CONSTANT_EMPTY_STR); + PyObject *bytes = curses_window_instr_bytes(self, args, + "_curses.window.in_wstr"); + if (bytes == NULL) { + return NULL; } - PyObject *res = PyUnicode_Decode(buf, strlen(buf), self->encoding, NULL); - PyMem_Free(buf); + PyObject *res = PyUnicode_Decode(PyBytes_AS_STRING(bytes), + PyBytes_GET_SIZE(bytes), + self->encoding, NULL); + Py_DECREF(bytes); return res; #endif } @@ -4835,12 +4879,10 @@ static PyMethodDef PyCursesWindow_methods[] = { "getstr", PyCursesWindow_getstr, METH_VARARGS, _curses_window_getstr__doc__ }, -#ifdef HAVE_NCURSESW { "get_wstr", PyCursesWindow_get_wstr, METH_VARARGS, _curses_window_get_wstr__doc__ }, -#endif {"getyx", PyCursesWindow_getyx, METH_NOARGS, "getyx($self, /)\n--\n\n" "Return a tuple (y, x) of the current cursor position."}, @@ -5686,7 +5728,6 @@ _curses_erasechar_impl(PyObject *module) return PyBytes_FromStringAndSize(&ch, 1); } -#ifdef HAVE_NCURSESW /*[clinic input] _curses.erasewchar @@ -5697,17 +5738,24 @@ static PyObject * _curses_erasewchar_impl(PyObject *module) /*[clinic end generated code: output=7f3bd8c9097ac456 input=f7e9a3893b4df2f8]*/ { - wchar_t ch; - PyCursesStatefulInitialised(module); +#ifdef HAVE_NCURSESW + wchar_t ch; + if (erasewchar(&ch) == ERR) { curses_set_error(module, "erasewchar", NULL); return NULL; } return PyUnicode_FromWideChar(&ch, 1); +#else + /* Without the wide library, decode the single-byte erase character + with the screen's encoding. */ + char ch = erasechar(); + + return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL); +#endif } -#endif /* HAVE_NCURSESW */ /*[clinic input] _curses.flash @@ -6890,7 +6938,6 @@ _curses_killchar_impl(PyObject *module) return PyBytes_FromStringAndSize(&ch, 1); } -#ifdef HAVE_NCURSESW /*[clinic input] _curses.killwchar @@ -6901,6 +6948,7 @@ static PyObject * _curses_killwchar_impl(PyObject *module) /*[clinic end generated code: output=eac1fd72a0c88d42 input=5c2d7d1ab2f24eb7]*/ { +#ifdef HAVE_NCURSESW wchar_t ch; if (killwchar(&ch) == ERR) { @@ -6908,8 +6956,14 @@ _curses_killwchar_impl(PyObject *module) return NULL; } return PyUnicode_FromWideChar(&ch, 1); +#else + /* Without the wide library, decode the single-byte kill character + with the screen's encoding. */ + char ch = killchar(); + + return PyUnicode_Decode(&ch, 1, curses_screen_encoding, NULL); +#endif } -#endif /* HAVE_NCURSESW */ /*[clinic input] _curses.longname @@ -7783,7 +7837,6 @@ _curses_unctrl(PyObject *module, PyObject *ch) return PyBytes_FromString(res); } -#ifdef HAVE_NCURSESW /*[clinic input] _curses.wunctrl @@ -7800,12 +7853,13 @@ static PyObject * _curses_wunctrl(PyObject *module, PyObject *ch) /*[clinic end generated code: output=7b16d5534ff05728 input=9ceb6749118bd07c]*/ { + PyCursesStatefulInitialised(module); + +#ifdef HAVE_NCURSESW chtype ch_; wchar_t wstr[CCHARW_MAX + 1]; cchar_t wcval; - PyCursesStatefulInitialised(module); - int type = PyCurses_ConvertToCchar_t(NULL, ch, &ch_, wstr); if (type == 0) { return NULL; @@ -7826,33 +7880,24 @@ _curses_wunctrl(PyObject *module, PyObject *ch) return NULL; } return PyUnicode_FromWideChar(res, -1); -} -#endif /* HAVE_NCURSESW */ - -/*[clinic input] -_curses.ungetch - - ch: object - / - -Push ch so the next getch() will return it. -[clinic start generated code]*/ - -static PyObject * -_curses_ungetch(PyObject *module, PyObject *ch) -/*[clinic end generated code: output=9b19d8268376d887 input=6681e6ae4c42e5eb]*/ -{ +#else + /* Without the wide library, fall back to the single-byte unctrl() and + decode its result with the screen's encoding. */ chtype ch_; - PyCursesStatefulInitialised(module); - - if (!PyCurses_ConvertToChtype(NULL, ch, &ch_)) + if (!PyCurses_ConvertToChtype(NULL, ch, &ch_)) { return NULL; + } - return curses_check_err(module, ungetch(ch_), "ungetch", NULL); + const char *res = unctrl(ch_); + if (res == NULL) { + curses_set_null_error(module, "unctrl", "wunctrl"); + return NULL; + } + return PyUnicode_Decode(res, strlen(res), curses_screen_encoding, NULL); +#endif } -#ifdef HAVE_NCURSESW /* Convert an object to a character (wchar_t): - int @@ -7900,6 +7945,40 @@ PyCurses_ConvertToWchar_t(PyObject *obj, } } +/*[clinic input] +_curses.ungetch + + ch: object + / + +Push ch so the next getch() will return it. +[clinic start generated code]*/ + +static PyObject * +_curses_ungetch(PyObject *module, PyObject *ch) +/*[clinic end generated code: output=9b19d8268376d887 input=6681e6ae4c42e5eb]*/ +{ + PyCursesStatefulInitialised(module); + +#ifdef HAVE_NCURSESW + /* Push a str through the wide queue, so a character that does not fit in a + single byte round-trips to get_wch(). An int stays a raw value (a keycode + or a byte) for getch(); use unget_wch() to push it as a character. */ + if (PyUnicode_Check(ch)) { + wchar_t wch; + if (!PyCurses_ConvertToWchar_t(ch, &wch)) + return NULL; + return curses_check_err(module, unget_wch(wch), "unget_wch", "ungetch"); + } +#endif + + chtype ch_; + if (!PyCurses_ConvertToChtype(NULL, ch, &ch_)) + return NULL; + + return curses_check_err(module, ungetch(ch_), "ungetch", NULL); +} + /*[clinic input] _curses.unget_wch @@ -7919,9 +7998,33 @@ _curses_unget_wch(PyObject *module, PyObject *ch) if (!PyCurses_ConvertToWchar_t(ch, &wch)) return NULL; +#ifdef HAVE_NCURSESW return curses_check_err(module, unget_wch(wch), "unget_wch", NULL); -} +#else + /* Without the wide library there is no unget_wch(): encode the character as + a single screen-encoding byte and push that. Narrow builds support only + 8-bit locales, so a character that does not fit in one byte is rejected. */ + PyObject *str = PyUnicode_FromWideChar(&wch, 1); + if (str == NULL) { + return NULL; + } + PyObject *bytes = PyUnicode_AsEncodedString(str, curses_screen_encoding, + NULL); + Py_DECREF(str); + if (bytes == NULL) { + return NULL; + } + if (PyBytes_GET_SIZE(bytes) != 1) { + Py_DECREF(bytes); + PyErr_SetString(PyExc_OverflowError, + "character does not fit in a single byte"); + return NULL; + } + int b = (unsigned char)PyBytes_AS_STRING(bytes)[0]; + Py_DECREF(bytes); + return curses_check_err(module, ungetch(b), "ungetch", "unget_wch"); #endif +} #ifdef HAVE_CURSES_USE_ENV /*[clinic input] diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index f895090bef845f..8e0f922ad0444f 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -1271,8 +1271,6 @@ _curses_window_getkey(PyObject *self, PyObject *args) return return_value; } -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_window_get_wch__doc__, "get_wch([y, x])\n" "Get a wide character from terminal keyboard.\n" @@ -1319,8 +1317,6 @@ _curses_window_get_wch(PyObject *self, PyObject *args) return return_value; } -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_window_hline__doc__, "hline([y, x,] ch, n, [attr])\n" "Display a horizontal line.\n" @@ -2784,8 +2780,6 @@ _curses_erasechar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_erasechar_impl(module); } -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_erasewchar__doc__, "erasewchar($module, /)\n" "--\n" @@ -2804,8 +2798,6 @@ _curses_erasewchar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_erasewchar_impl(module); } -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_flash__doc__, "flash($module, /)\n" "--\n" @@ -4097,8 +4089,6 @@ _curses_killchar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_killchar_impl(module); } -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_killwchar__doc__, "killwchar($module, /)\n" "--\n" @@ -4117,8 +4107,6 @@ _curses_killwchar(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_killwchar_impl(module); } -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_longname__doc__, "longname($module, /)\n" "--\n" @@ -5316,8 +5304,6 @@ PyDoc_STRVAR(_curses_unctrl__doc__, #define _CURSES_UNCTRL_METHODDEF \ {"unctrl", (PyCFunction)_curses_unctrl, METH_O, _curses_unctrl__doc__}, -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_wunctrl__doc__, "wunctrl($module, ch, /)\n" "--\n" @@ -5330,8 +5316,6 @@ PyDoc_STRVAR(_curses_wunctrl__doc__, #define _CURSES_WUNCTRL_METHODDEF \ {"wunctrl", (PyCFunction)_curses_wunctrl, METH_O, _curses_wunctrl__doc__}, -#endif /* defined(HAVE_NCURSESW) */ - PyDoc_STRVAR(_curses_ungetch__doc__, "ungetch($module, ch, /)\n" "--\n" @@ -5341,8 +5325,6 @@ PyDoc_STRVAR(_curses_ungetch__doc__, #define _CURSES_UNGETCH_METHODDEF \ {"ungetch", (PyCFunction)_curses_ungetch, METH_O, _curses_ungetch__doc__}, -#if defined(HAVE_NCURSESW) - PyDoc_STRVAR(_curses_unget_wch__doc__, "unget_wch($module, ch, /)\n" "--\n" @@ -5352,8 +5334,6 @@ PyDoc_STRVAR(_curses_unget_wch__doc__, #define _CURSES_UNGET_WCH_METHODDEF \ {"unget_wch", (PyCFunction)_curses_unget_wch, METH_O, _curses_unget_wch__doc__}, -#endif /* defined(HAVE_NCURSESW) */ - #if defined(HAVE_CURSES_USE_ENV) PyDoc_STRVAR(_curses_use_env__doc__, @@ -5487,10 +5467,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_WINDOW_ENCLOSE_METHODDEF #endif /* !defined(_CURSES_WINDOW_ENCLOSE_METHODDEF) */ -#ifndef _CURSES_WINDOW_GET_WCH_METHODDEF - #define _CURSES_WINDOW_GET_WCH_METHODDEF -#endif /* !defined(_CURSES_WINDOW_GET_WCH_METHODDEF) */ - #ifndef _CURSES_WINDOW_NOUTREFRESH_METHODDEF #define _CURSES_WINDOW_NOUTREFRESH_METHODDEF #endif /* !defined(_CURSES_WINDOW_NOUTREFRESH_METHODDEF) */ @@ -5519,10 +5495,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_IS_RAW_METHODDEF #endif /* !defined(_CURSES_IS_RAW_METHODDEF) */ -#ifndef _CURSES_ERASEWCHAR_METHODDEF - #define _CURSES_ERASEWCHAR_METHODDEF -#endif /* !defined(_CURSES_ERASEWCHAR_METHODDEF) */ - #ifndef _CURSES_GETSYX_METHODDEF #define _CURSES_GETSYX_METHODDEF #endif /* !defined(_CURSES_GETSYX_METHODDEF) */ @@ -5591,10 +5563,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_IS_TERM_RESIZED_METHODDEF #endif /* !defined(_CURSES_IS_TERM_RESIZED_METHODDEF) */ -#ifndef _CURSES_KILLWCHAR_METHODDEF - #define _CURSES_KILLWCHAR_METHODDEF -#endif /* !defined(_CURSES_KILLWCHAR_METHODDEF) */ - #ifndef _CURSES_MOUSEINTERVAL_METHODDEF #define _CURSES_MOUSEINTERVAL_METHODDEF #endif /* !defined(_CURSES_MOUSEINTERVAL_METHODDEF) */ @@ -5623,14 +5591,6 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #define _CURSES_TYPEAHEAD_METHODDEF #endif /* !defined(_CURSES_TYPEAHEAD_METHODDEF) */ -#ifndef _CURSES_WUNCTRL_METHODDEF - #define _CURSES_WUNCTRL_METHODDEF -#endif /* !defined(_CURSES_WUNCTRL_METHODDEF) */ - -#ifndef _CURSES_UNGET_WCH_METHODDEF - #define _CURSES_UNGET_WCH_METHODDEF -#endif /* !defined(_CURSES_UNGET_WCH_METHODDEF) */ - #ifndef _CURSES_USE_ENV_METHODDEF #define _CURSES_USE_ENV_METHODDEF #endif /* !defined(_CURSES_USE_ENV_METHODDEF) */ @@ -5642,4 +5602,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=bbf6d77a5813b1e1 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=f48f8e3554b30b86 input=a9049054013a1b77]*/