| import functools | |
| import inspect | |
| import os | |
| import platform | |
| import select | |
| import string | |
| import sys | |
| import tempfile | |
| import threading | |
| import unittest | |
| from unittest.mock import MagicMock | |
| from test.support import (requires, verbose, SaveSignals, cpython_only, | |
| check_disallow_instantiation, MISSING_C_DOCSTRINGS, | |
| gc_collect, SHORT_TIMEOUT) | |
| from test.support.import_helper import import_module | |
| from test.support import os_helper | |
| # Optionally test curses module. This currently requires that the | |
| # 'curses' resource be given on the regrtest command line using the -u | |
| # option. If not available, nothing after this line will be executed. | |
| requires('curses') | |
| # If either of these don't exist, skip the tests. | |
| curses = import_module('curses') | |
| import_module('curses.ascii') | |
| import_module('curses.textpad') | |
| try: | |
| import curses.panel | |
| except ImportError: | |
| pass | |
| # Only reachable once curses imported, so the platform has fcntl too. | |
| import fcntl | |
| def requires_curses_func(name): | |
| return unittest.skipUnless(hasattr(curses, name), | |
| 'requires curses.%s' % name) | |
| def requires_curses_window_meth(name): | |
| def deco(test): | |
| @functools.wraps(test) | |
| def wrapped(self, *args, **kwargs): | |
| if not hasattr(self.stdscr, name): | |
| raise unittest.SkipTest('requires curses.window.%s' % name) | |
| test(self, *args, **kwargs) | |
| return wrapped | |
| return deco | |
| WIDE_BUILD = import_module('_curses')._wide_character_support | |
| def encodable(s, encoding): | |
| # Wide characters are only supported in a locale that can encode them. | |
| try: | |
| s.encode(encoding) | |
| except UnicodeEncodeError: | |
| return False | |
| return True | |
| def storable(s, encoding): | |
| # Text the current build can place in character cells. A wide build | |
| # stores any locale-encodable text (combining sequences and multibyte | |
| # characters included). A narrow build has no wide-character cells, so | |
| # each character must occupy a single cell -- that is, encode to exactly | |
| # one byte. | |
| if not encodable(s, encoding): | |
| return False | |
| if WIDE_BUILD: | |
| return True | |
| return len(s.encode(encoding)) == len(s) | |
| 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 | |
| # The WACS_* double-line and thick-line character cells, without the common | |
| # prefix, paired with the alternate name spelling out their four sides | |
| # (blank, double or thick, clockwise from the top). | |
| WACS_LINE_ALIASES = [ | |
| ('D_ULCORNER', 'BDDB'), ('D_LLCORNER', 'DDBB'), | |
| ('D_URCORNER', 'BBDD'), ('D_LRCORNER', 'DBBD'), | |
| ('D_LTEE', 'DDDB'), ('D_RTEE', 'DBDD'), | |
| ('D_BTEE', 'DDBD'), ('D_TTEE', 'BDDD'), | |
| ('D_HLINE', 'BDBD'), ('D_VLINE', 'DBDB'), ('D_PLUS', 'DDDD'), | |
| ('T_ULCORNER', 'BTTB'), ('T_LLCORNER', 'TTBB'), | |
| ('T_URCORNER', 'BBTT'), ('T_LRCORNER', 'TBBT'), | |
| ('T_LTEE', 'TTTB'), ('T_RTEE', 'TBTT'), | |
| ('T_BTEE', 'TTBT'), ('T_TTEE', 'BTTT'), | |
| ('T_HLINE', 'BTBT'), ('T_VLINE', 'TBTB'), ('T_PLUS', 'TTTT'), | |
| ] | |
| def requires_colors(test): | |
| @functools.wraps(test) | |
| def wrapped(self, *args, **kwargs): | |
| if not curses.has_colors(): | |
| self.skipTest('requires colors support') | |
| curses.start_color() | |
| test(self, *args, **kwargs) | |
| return wrapped | |
| term = os.environ.get('TERM') | |
| SHORT_MAX = 0x7fff | |
| # ncurses before 6.5, and the native curses of NetBSD and illumos/Solaris, | |
| # crash on repeated newterm()/delscreen(); fall back to initscr() and skip the | |
| # multi-screen tests. The native ones are keyed off the platform so a fixed | |
| # version can be excluded later. | |
| _ncurses_version = getattr(curses, 'ncurses_version', None) | |
| if _ncurses_version is not None: | |
| BROKEN_NEWTERM = _ncurses_version < (6, 5) | |
| else: | |
| BROKEN_NEWTERM = sys.platform.startswith(('netbsd', 'sunos')) | |
| USE_NEWTERM = hasattr(curses, 'newterm') and not BROKEN_NEWTERM | |
| # Older macOS reports a variation selector as a spacing character (wcwidth() | |
| # == 1) rather than a combining mark, so it cannot share a cell with its base. | |
| # The failure is confirmed on 14.2 and gone by 26, so skip below 26. | |
| def _broken_variation_selector_width(): | |
| if sys.platform == 'darwin': | |
| mac_ver = platform.mac_ver()[0] | |
| if mac_ver: | |
| return tuple(map(int, mac_ver.split('.'))) < (26,) | |
| return False | |
| BROKEN_VARIATION_SELECTOR_WIDTH = _broken_variation_selector_width() | |
| class CustomError(Exception): | |
| pass | |
| # newterm() is used when available (it reports errors instead of exiting), but | |
| # initscr() is still the fallback, and an unusable $TERM has no terminal to | |
| # drive either way. | |
| @unittest.skipIf(not term or term == 'unknown', | |
| "$TERM=%r, no usable terminal" % term) | |
| @unittest.skipIf(sys.platform == "cygwin", | |
| "cygwin's curses mostly just hangs") | |
| class TestCurses(unittest.TestCase): | |
| @classmethod | |
| def setUpClass(cls): | |
| if verbose: | |
| print(f'TERM={term}', file=sys.stderr, flush=True) | |
| # testing setupterm() inside initscr/endwin | |
| # causes terminal breakage | |
| stdout_fd = sys.__stdout__.fileno() | |
| curses.setupterm(fd=stdout_fd) | |
| def setUp(self): | |
| self.isatty = True | |
| self.output = sys.__stdout__ | |
| stdout_fd = sys.__stdout__.fileno() | |
| if not sys.__stdout__.isatty(): | |
| # initstr() unconditionally uses C stdout. | |
| # If it is redirected to file or pipe, try to attach it | |
| # to terminal. | |
| # First, save a copy of the file descriptor of stdout, so it | |
| # can be restored after finishing the test. | |
| dup_fd = os.dup(stdout_fd) | |
| self.addCleanup(os.close, dup_fd) | |
| self.addCleanup(os.dup2, dup_fd, stdout_fd) | |
| if sys.__stderr__.isatty(): | |
| # If stderr is connected to terminal, use it. | |
| tmp = sys.__stderr__ | |
| self.output = sys.__stderr__ | |
| else: | |
| try: | |
| # Try to open the terminal device. | |
| tmp = open('/dev/tty', 'wb', buffering=0) | |
| except OSError: | |
| # As a fallback, use regular file to write control codes. | |
| # Some functions (like savetty) will not work, but at | |
| # least the garbage control sequences will not be mixed | |
| # with the testing report. | |
| tmp = tempfile.TemporaryFile(mode='wb', buffering=0) | |
| self.isatty = False | |
| self.addCleanup(tmp.close) | |
| self.output = None | |
| os.dup2(tmp.fileno(), stdout_fd) | |
| self.save_signals = SaveSignals() | |
| self.save_signals.save() | |
| self.addCleanup(self.save_signals.restore) | |
| if verbose and self.output is not None: | |
| # just to make the test output a little more readable | |
| sys.stderr.flush() | |
| sys.stdout.flush() | |
| print(file=self.output, flush=True) | |
| if USE_NEWTERM: | |
| # Use newterm() rather than initscr(): it reports errors instead of | |
| # exiting, and gives each test a fresh screen, which also lets | |
| # ScreenTests run newterm()/set_term() in the same process. | |
| try: | |
| infd = sys.__stdin__.fileno() | |
| if fcntl.fcntl(infd, fcntl.F_GETFL) & os.O_ACCMODE == os.O_WRONLY: | |
| # newterm() needs a readable input fd; a write-only stdin | |
| # (as nohup leaves for a backgrounded run) fails with EINVAL. | |
| infd = stdout_fd | |
| except (AttributeError, ValueError, OSError): | |
| infd = stdout_fd | |
| self.screen = curses.newterm(term, stdout_fd, infd) | |
| self.stdscr = self.screen.stdscr | |
| # Close the screen after the test to break its window<->screen | |
| # reference cycle deterministically, rather than leaving it for the | |
| # cyclic GC to collect during a much later test (where a window's | |
| # delwin() can fail -- an unraisable error on macOS). | |
| self.addCleanup(self.screen.close) | |
| self.addCleanup(setattr, self, 'screen', None) | |
| self.addCleanup(setattr, self, 'stdscr', None) | |
| else: | |
| # Tests share one initscr() screen; clear the rendition and | |
| # background so a previous test's does not bleed in. | |
| self.stdscr = curses.initscr() | |
| self.stdscr.attrset(curses.A_NORMAL) | |
| self.stdscr.bkgdset(' ') | |
| if self.isatty: | |
| curses.savetty() | |
| self.addCleanup(curses.endwin) | |
| self.addCleanup(curses.resetty) | |
| self.stdscr.erase() | |
| @requires_curses_func('filter') | |
| def test_filter(self): | |
| # filter() must be called before initscr()/newterm(); it confines | |
| # curses to a single line. Undo it with nofilter() afterwards so that | |
| # it does not shrink the screens created by later tests. | |
| curses.filter() | |
| if hasattr(curses, 'nofilter'): | |
| self.addCleanup(curses.nofilter) | |
| @requires_curses_func('use_env') | |
| def test_use_env(self): | |
| # TODO: Should be called before initscr() or newterm() are called. | |
| # TODO: use_tioctl() | |
| curses.use_env(False) | |
| curses.use_env(True) | |
| def test_error(self): | |
| self.assertIsSubclass(curses.error, Exception) | |
| def test_create_windows(self): | |
| win = curses.newwin(5, 10) | |
| self.assertEqual(win.getbegyx(), (0, 0)) | |
| self.assertEqual(win.getparyx(), (-1, -1)) | |
| self.assertEqual(win.getmaxyx(), (5, 10)) | |
| win = curses.newwin(10, 15, 2, 5) | |
| self.assertEqual(win.getbegyx(), (2, 5)) | |
| self.assertEqual(win.getparyx(), (-1, -1)) | |
| self.assertEqual(win.getmaxyx(), (10, 15)) | |
| win2 = win.subwin(3, 7) | |
| self.assertEqual(win2.getbegyx(), (3, 7)) | |
| self.assertEqual(win2.getparyx(), (1, 2)) | |
| self.assertEqual(win2.getmaxyx(), (9, 13)) | |
| win2 = win.subwin(5, 10, 3, 7) | |
| self.assertEqual(win2.getbegyx(), (3, 7)) | |
| self.assertEqual(win2.getparyx(), (1, 2)) | |
| self.assertEqual(win2.getmaxyx(), (5, 10)) | |
| win3 = win.derwin(2, 3) | |
| self.assertEqual(win3.getbegyx(), (4, 8)) | |
| self.assertEqual(win3.getparyx(), (2, 3)) | |
| self.assertEqual(win3.getmaxyx(), (8, 12)) | |
| win3 = win.derwin(6, 11, 2, 3) | |
| self.assertEqual(win3.getbegyx(), (4, 8)) | |
| self.assertEqual(win3.getparyx(), (2, 3)) | |
| self.assertEqual(win3.getmaxyx(), (6, 11)) | |
| win.mvwin(0, 1) | |
| self.assertEqual(win.getbegyx(), (0, 1)) | |
| self.assertEqual(win.getparyx(), (-1, -1)) | |
| self.assertEqual(win.getmaxyx(), (10, 15)) | |
| self.assertEqual(win2.getbegyx(), (3, 7)) | |
| self.assertEqual(win2.getparyx(), (1, 2)) | |
| self.assertEqual(win2.getmaxyx(), (5, 10)) | |
| self.assertEqual(win3.getbegyx(), (4, 8)) | |
| self.assertEqual(win3.getparyx(), (2, 3)) | |
| self.assertEqual(win3.getmaxyx(), (6, 11)) | |
| win2.mvderwin(2, 1) | |
| self.assertEqual(win2.getbegyx(), (3, 7)) | |
| self.assertEqual(win2.getparyx(), (2, 1)) | |
| self.assertEqual(win2.getmaxyx(), (5, 10)) | |
| win3.mvderwin(2, 1) | |
| self.assertEqual(win3.getbegyx(), (4, 8)) | |
| self.assertEqual(win3.getparyx(), (2, 1)) | |
| self.assertEqual(win3.getmaxyx(), (6, 11)) | |
| def test_subwindows_references(self): | |
| win = curses.newwin(5, 10) | |
| win2 = win.subwin(3, 7) | |
| del win | |
| gc_collect() | |
| del win2 | |
| gc_collect() | |
| def test_getparent(self): | |
| # getparent() calls no curses function, so it works with any backend | |
| # and is not gated like the is_*() getters below. | |
| stdscr = self.stdscr | |
| self.assertIsNone(stdscr.getparent()) | |
| sub = stdscr.subwin(3, 3, 0, 0) | |
| self.assertIs(sub.getparent(), stdscr) | |
| def test_dupwin(self): | |
| win = curses.newwin(5, 10, 2, 3) | |
| win.addstr(0, 0, 'ABCDE') | |
| win.addstr(1, 0, 'fghij') | |
| dup = win.dupwin() | |
| # Same geometry and contents as the original. | |
| self.assertEqual(dup.getbegyx(), win.getbegyx()) | |
| self.assertEqual(dup.getmaxyx(), win.getmaxyx()) | |
| self.assertEqual(dup.instr(0, 0, 5), b'ABCDE') | |
| self.assertEqual(dup.instr(1, 0, 5), b'fghij') | |
| # The duplicate is independent, not a subwindow. | |
| if hasattr(dup, 'is_subwin'): | |
| self.assertIs(dup.is_subwin(), False) | |
| self.assertIsNone(dup.getparent()) | |
| # Changes to one do not affect the other. | |
| dup.addstr(0, 0, 'xxxxx') | |
| win.addstr(1, 0, 'YYYYY') | |
| self.assertEqual(win.instr(0, 0, 5), b'ABCDE') | |
| self.assertEqual(dup.instr(0, 0, 5), b'xxxxx') | |
| self.assertEqual(dup.instr(1, 0, 5), b'fghij') | |
| self.assertEqual(win.instr(1, 0, 5), b'YYYYY') | |
| # A subwindow can also be duplicated; the duplicate is independent. | |
| sub = win.subwin(3, 5, 2, 3) | |
| subdup = sub.dupwin() | |
| self.assertEqual(subdup.getmaxyx(), sub.getmaxyx()) | |
| if hasattr(subdup, 'is_subwin'): | |
| self.assertIs(subdup.is_subwin(), False) | |
| self.assertIsNone(subdup.getparent()) | |
| def test_move_cursor(self): | |
| stdscr = self.stdscr | |
| win = stdscr.subwin(10, 15, 2, 5) | |
| stdscr.move(1, 2) | |
| win.move(2, 4) | |
| self.assertEqual(stdscr.getyx(), (1, 2)) | |
| self.assertEqual(win.getyx(), (2, 4)) | |
| win.cursyncup() | |
| self.assertEqual(stdscr.getyx(), (4, 9)) | |
| def test_refresh_control(self): | |
| stdscr = self.stdscr | |
| # touchwin()/untouchwin()/is_wintouched() | |
| stdscr.refresh() | |
| self.assertIs(stdscr.is_wintouched(), False) | |
| stdscr.touchwin() | |
| self.assertIs(stdscr.is_wintouched(), True) | |
| stdscr.refresh() | |
| self.assertIs(stdscr.is_wintouched(), False) | |
| stdscr.touchwin() | |
| self.assertIs(stdscr.is_wintouched(), True) | |
| stdscr.untouchwin() | |
| self.assertIs(stdscr.is_wintouched(), False) | |
| # touchline()/untouchline()/is_linetouched() | |
| stdscr.touchline(5, 2) | |
| self.assertIs(stdscr.is_linetouched(5), True) | |
| self.assertIs(stdscr.is_linetouched(6), True) | |
| self.assertIs(stdscr.is_wintouched(), True) | |
| stdscr.touchline(5, 1, False) | |
| self.assertIs(stdscr.is_linetouched(5), False) | |
| # syncup() | |
| win = stdscr.subwin(10, 15, 2, 5) | |
| win2 = win.subwin(5, 10, 3, 7) | |
| win2.touchwin() | |
| stdscr.untouchwin() | |
| win2.syncup() | |
| self.assertIs(win.is_wintouched(), True) | |
| self.assertIs(stdscr.is_wintouched(), True) | |
| # syncdown() | |
| stdscr.touchwin() | |
| win.untouchwin() | |
| win2.untouchwin() | |
| win2.syncdown() | |
| self.assertIs(win2.is_wintouched(), True) | |
| # syncok() | |
| if hasattr(stdscr, 'syncok') and not sys.platform.startswith("sunos"): | |
| win.untouchwin() | |
| stdscr.untouchwin() | |
| for syncok in [False, True]: | |
| win2.syncok(syncok) | |
| win2.addch('a') | |
| self.assertIs(win.is_wintouched(), syncok) | |
| self.assertIs(stdscr.is_wintouched(), syncok) | |
| # Many tests below use a common set of non-ASCII cases, each applied only | |
| # when the window encoding can represent it -- so the whole suite is meant to | |
| # be run under several locales (e.g. ISO-8859-1, ISO-8859-15, KOI8-U): | |
| # 'A'/'a' ASCII | |
| # 'é' common to the Latin encodings | |
| # '¤'/'€'/'є' byte 0xA4 in ISO-8859-1 / ISO-8859-15 / KOI8-U | |
| # Precomposed characters are used so a round-trip does not depend on the form. | |
| # On a narrow (non-wide) build a cell holds one byte, so cases that need a | |
| # combining sequence or a multibyte character are guarded with _storable(). | |
| def _encodable(self, s): | |
| return encodable(s, self.stdscr.encoding) | |
| def _storable(self, s): | |
| return storable(s, self.stdscr.encoding) | |
| def _char_code(self, ch): | |
| # The integer the int-input API (addch(int), do_command()) uses for a | |
| # character, or None if it has none: a cell holds a single locale byte. | |
| try: | |
| b = ch.encode(self.stdscr.encoding) | |
| except UnicodeEncodeError: | |
| return None | |
| return b[0] if len(b) == 1 else None | |
| def _read_char(self, y, x): | |
| # The character written to a cell, read back for output checks. inch() | |
| # is unusable here: on a wide build it returns the low 8 bits of the | |
| # character's code point rather than its locale-encoded byte, mangling | |
| # anything outside Latin-1. in_wch() reads the wide cell directly; | |
| # without it, instr() re-encodes the cell to the window encoding. | |
| stdscr = self.stdscr | |
| if hasattr(stdscr, 'in_wch'): | |
| return str(stdscr.in_wch(y, x)) | |
| return stdscr.instr(y, x, 1).decode(stdscr.encoding) | |
| @requires_wide_build | |
| def test_addch_combining(self): | |
| stdscr = self.stdscr | |
| stdscr.move(0, 0) | |
| # A character cell may hold a spacing char plus combining marks. | |
| if self._encodable('e\u0301'): | |
| stdscr.addch('e\u0301') # 'e' + COMBINING ACUTE ACCENT | |
| if self._encodable('a\u0323\u0300'): | |
| stdscr.addch(1, 0, 'a\u0323\u0300') # base plus two combining marks | |
| # Too many code points to fit in a single character cell. | |
| self.assertRaises(TypeError, stdscr.addch, 'e' + '\u0301' * 10) | |
| # Only the first code point may be a spacing character. | |
| self.assertRaises(ValueError, stdscr.addch, 'ab') | |
| self.assertRaises(ValueError, stdscr.addch, 'a\u0301b') | |
| # A lone control character is allowed (like addch(ord('\n'))), but it | |
| # cannot be combined with other characters, as base or otherwise. | |
| stdscr.addch('\n') | |
| self.assertRaises(ValueError, stdscr.addch, 'a\n') | |
| self.assertRaises(ValueError, stdscr.addch, '\n\u0301') | |
| self.assertRaises(ValueError, stdscr.addch, '\ne\u0301') | |
| @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, | |
| # as does an emoji with a zero-width variation selector. | |
| stdscr = self.stdscr | |
| if self._encodable('\U0001f600'): | |
| stdscr.addch(0, 0, '\U0001f600') # single emoji | |
| # Skip the variation selector where the platform reports it as spacing. | |
| if not BROKEN_VARIATION_SELECTOR_WIDTH and self._encodable('\u263a\ufe0f'): | |
| stdscr.addch(1, 0, '\u263a\ufe0f') # WHITE SMILING FACE + VS-16 | |
| # An emoji ZWJ sequence or an emoji with a modifier is more than one | |
| # spacing character and cannot share a single cell. | |
| self.assertRaises(ValueError, stdscr.addch, | |
| '\U0001f44d\U0001f3fd') # thumbs up + skin tone | |
| self.assertRaises(ValueError, stdscr.addch, | |
| '\U0001f468\u200d\U0001f469') # man ZWJ woman | |
| @requires_wide_build | |
| def test_wide_characters(self): | |
| # Wide and combining characters in the character-cell methods. | |
| stdscr = self.stdscr | |
| combining = 'e\u0301' # 'e' + COMBINING ACUTE ACCENT | |
| vline, hline = '\u2502', '\u2500' # box-drawing vertical/horizontal | |
| stdscr.move(0, 0) | |
| if self._encodable(combining): | |
| stdscr.echochar(combining) | |
| stdscr.insch(1, 0, combining) | |
| stdscr.bkgdset(combining) | |
| stdscr.bkgd(combining) | |
| if self._encodable(hline): | |
| stdscr.hline(2, 0, hline, 5) | |
| if self._encodable(vline): | |
| stdscr.vline(3, 0, vline, 3) | |
| if self._encodable(vline + hline): | |
| stdscr.border(vline, vline, hline, hline) | |
| stdscr.box(vline, hline) | |
| # border() and box() cannot mix a complexchar with an integer | |
| # character; a wide string character is narrowed instead, which only | |
| # works if it is a single byte. | |
| self.assertRaises(TypeError, stdscr.box, | |
| curses.complexchar(vline), ord('-')) | |
| @requires_wide_build | |
| def test_border_default_characters(self): | |
| # 0 requests the default character, as an omitted argument does, | |
| # even in a border drawn with wide characters. | |
| win = curses.newwin(5, 10, 5, 2) | |
| maxy, maxx = win.getmaxyx() | |
| corners = [(0, 0), (0, maxx-1), (maxy-1, 0), (maxy-1, maxx-1)] | |
| win.border('|', '|', '-', '-', 0, 0, 0, 0) | |
| with_zeros = [win.in_wch(y, x) for y, x in corners] | |
| win.erase() | |
| win.border('|', '|', '-', '-') | |
| self.assertEqual([win.in_wch(y, x) for y, x in corners], with_zeros) | |
| win.border(0, '|', 0, '-', 0, 0, 0, 0) | |
| vline = curses.complexchar('|') | |
| hline = curses.complexchar('-') | |
| win.border(vline, vline, hline, hline, 0, 0, 0, 0) | |
| # box() takes 0 for either side, and draws the same default | |
| # characters as an omitted border() argument. | |
| win.erase() | |
| win.border('|', '|') | |
| default_corner = win.in_wch(0, 0) | |
| default_hline = win.in_wch(0, 1) | |
| win.erase() | |
| win.border(0, 0, '-', '-') | |
| default_vline = win.in_wch(1, 0) | |
| win.erase() | |
| win.box('|', 0) | |
| self.assertEqual(win.in_wch(0, 0), default_corner) | |
| self.assertEqual(win.in_wch(0, 1), default_hline) | |
| win.erase() | |
| win.box(0, '-') | |
| self.assertEqual(win.in_wch(1, 0), default_vline) | |
| win.box(vline, 0) | |
| @requires_wide_build | |
| def test_border_mixed_characters(self): | |
| # Integer and bytes characters other than 0 are only drawn by the | |
| # narrow function, which draws string characters as single bytes. | |
| win = curses.newwin(5, 10, 5, 2) | |
| win.border('|', '|', '-', '-', 65, 66, 67, 68) | |
| self.assertEqual(win.instr(0, 0), b'A--------B') | |
| self.assertEqual(win.instr(1, 0), b'| |') | |
| self.assertEqual(win.instr(4, 0), b'C--------D') | |
| win.border('|', b'!') | |
| self.assertEqual(win.instr(1, 0), b'| !') | |
| # b'\0' is a byte character, not the sentinel, but the narrow function | |
| # draws a zero character as the default one. | |
| win.border('|', b'\0') | |
| # A complexchar cannot be drawn as a byte. | |
| cc = curses.complexchar('|') | |
| self.assertRaises(TypeError, win.border, cc, 65) | |
| self.assertRaises(TypeError, win.border, cc, b'!') | |
| # Neither can a string character that is not a single byte. | |
| vline = '\u2502' | |
| if len(vline.encode(win.encoding, 'replace')) != 1: | |
| self.assertRaises(OverflowError, win.border, vline, 65) | |
| # box() follows the same rules. | |
| win.box('|', 45) | |
| self.assertEqual(win.instr(1, 0), b'| |') | |
| win.box(b'|', '-') | |
| self.assertRaises(TypeError, win.box, cc, 45) | |
| self.assertRaises(TypeError, win.box, cc, b'-') | |
| if len(vline.encode(win.encoding, 'replace')) != 1: | |
| self.assertRaises(OverflowError, win.box, vline, 45) | |
| @requires_wide_build | |
| def test_wacs_constants(self): | |
| # Every ACS_* code has a WACS_* character cell counterpart, plus the | |
| # double-line and thick-line codes, which have no ACS_* counterpart. | |
| acs = {name.removeprefix('ACS_') | |
| for name in dir(curses) if name.startswith('ACS_')} | |
| wacs = {name.removeprefix('WACS_') | |
| for name in dir(curses) if name.startswith('WACS_')} | |
| extra = {name for pair in WACS_LINE_ALIASES for name in pair} | |
| self.assertEqual(wacs - extra, acs) | |
| for name in sorted(wacs): | |
| with self.subTest(name=name): | |
| self.assertIsInstance(getattr(curses, 'WACS_' + name), | |
| curses.complexchar) | |
| # The alternate names refer to the same cells. | |
| self.assertEqual(curses.WACS_BSSB, curses.WACS_ULCORNER) | |
| self.assertEqual(curses.WACS_BSBS, curses.WACS_HLINE) | |
| self.assertEqual(curses.WACS_SBSB, curses.WACS_VLINE) | |
| self.assertEqual(curses.WACS_SSSS, curses.WACS_PLUS) | |
| @requires_wide_build | |
| def test_wacs_line_constants(self): | |
| # The double-line and thick-line codes are optional, but a supporting | |
| # implementation provides the whole family under both names. | |
| present = [name for name, alias in WACS_LINE_ALIASES | |
| if hasattr(curses, 'WACS_' + name)] | |
| if not present: | |
| self.skipTest('requires double-line and thick-line characters') | |
| self.assertEqual(len(present), len(WACS_LINE_ALIASES)) | |
| for name, alias in WACS_LINE_ALIASES: | |
| with self.subTest(name=name): | |
| cell = getattr(curses, 'WACS_' + name) | |
| self.assertIsInstance(cell, curses.complexchar) | |
| self.assertEqual(getattr(curses, 'WACS_' + alias), cell) | |
| # They are distinct from the single-line characters. | |
| self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_HLINE) | |
| self.assertNotEqual(curses.WACS_T_HLINE, curses.WACS_HLINE) | |
| self.assertNotEqual(curses.WACS_D_HLINE, curses.WACS_T_HLINE) | |
| stdscr = self.stdscr | |
| stdscr.border(curses.WACS_D_VLINE, curses.WACS_D_VLINE, | |
| curses.WACS_D_HLINE, curses.WACS_D_HLINE, | |
| curses.WACS_D_ULCORNER, curses.WACS_D_URCORNER, | |
| curses.WACS_D_LLCORNER, curses.WACS_D_LRCORNER) | |
| self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_D_ULCORNER) | |
| self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_D_HLINE) | |
| @requires_wide_build | |
| def test_wacs_in_cell_methods(self): | |
| # A WACS_* cell can be used wherever a character cell is accepted. | |
| stdscr = self.stdscr | |
| stdscr.addch(0, 0, curses.WACS_ULCORNER) | |
| self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER) | |
| stdscr.insch(1, 0, curses.WACS_DIAMOND) | |
| self.assertEqual(stdscr.in_wch(1, 0), curses.WACS_DIAMOND) | |
| stdscr.hline(2, 0, curses.WACS_HLINE, 5) | |
| self.assertEqual(stdscr.in_wch(2, 4), curses.WACS_HLINE) | |
| stdscr.vline(3, 0, curses.WACS_VLINE, 3) | |
| self.assertEqual(stdscr.in_wch(5, 0), curses.WACS_VLINE) | |
| stdscr.border(curses.WACS_VLINE, curses.WACS_VLINE, | |
| curses.WACS_HLINE, curses.WACS_HLINE, | |
| curses.WACS_ULCORNER, curses.WACS_URCORNER, | |
| curses.WACS_LLCORNER, curses.WACS_LRCORNER) | |
| self.assertEqual(stdscr.in_wch(0, 0), curses.WACS_ULCORNER) | |
| stdscr.box(curses.WACS_VLINE, curses.WACS_HLINE) | |
| self.assertEqual(stdscr.in_wch(0, 1), curses.WACS_HLINE) | |
| 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. | |
| stdscr = self.stdscr | |
| cc = curses.complexchar('A', curses.A_BOLD) | |
| v = curses.complexchar('|') | |
| h = curses.complexchar('-') | |
| stdscr.move(0, 0) | |
| stdscr.addch(0, 0, cc) | |
| self.assertEqual(str(stdscr.in_wch(0, 0)), 'A') | |
| self.assertTrue(stdscr.in_wch(0, 0).attr & curses.A_BOLD) | |
| stdscr.insch(1, 0, cc) | |
| stdscr.echochar(cc) | |
| stdscr.bkgdset(cc) | |
| stdscr.bkgd(cc) | |
| stdscr.hline(2, 0, h, 3) | |
| stdscr.vline(3, 0, v, 3) | |
| stdscr.border(v, v, h, h) | |
| stdscr.box(v, h) | |
| # A complexchar already carries its rendition, so combining it with an | |
| # explicit attr argument is rejected. | |
| self.assertRaises(TypeError, stdscr.addch, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.addch, 0, 0, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.insch, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.echochar, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.bkgd, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.bkgdset, cc, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.hline, h, 3, curses.A_BOLD) | |
| self.assertRaises(TypeError, stdscr.vline, v, 3, curses.A_BOLD) | |
| def test_in_wstr(self): | |
| # The wide-character window read returns a str (instr returns bytes). | |
| # See _encodable for the character set. | |
| stdscr = self.stdscr | |
| for s in ['abz', # ASCII | |
| 'a\u00e9\u2502z', # acute e (precomposed), box vline | |
| 'na\u00efve', # common to the Latin encodings | |
| 'na\u00efve \u00a4', # ISO-8859-1 | |
| 'soup\u00e7on \u20ac', # ISO-8859-15 | |
| '\u0434\u044f\u043a']: # KOI8-U | |
| if self._storable(s): | |
| with self.subTest(s=s): | |
| stdscr.addstr(0, 0, s) | |
| self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) | |
| self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes) | |
| # Reading no characters gives an empty string, like instr() and | |
| # in_wchstr() do. curses does not terminate the buffer in this case. | |
| stdscr.addstr(0, 0, 'abz') | |
| self.assertEqual(stdscr.in_wstr(0, 0, 0), '') | |
| self.assertEqual(stdscr.in_wstr(0), '') | |
| self.assertEqual(stdscr.in_wstr(0, 0, 2**31), stdscr.in_wstr(0, 0)) | |
| self.assertRaises(OverflowError, stdscr.in_wstr, 2**1000) | |
| self.assertRaises(ValueError, stdscr.in_wstr, -2) | |
| self.assertRaises(ValueError, stdscr.in_wstr, 0, 2, -2) | |
| self.assertRaises(ValueError, stdscr.in_wstr, -2**1000) | |
| 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. | |
| cc = curses.complexchar('A', curses.A_BOLD) | |
| self.assertEqual(str(cc), 'A') | |
| self.assertTrue(cc.attr & curses.A_BOLD) | |
| self.assertEqual(cc.pair, 0) | |
| # A spacing character optionally followed by combining characters. | |
| if self._storable('e\u0301'): | |
| self.assertEqual(str(curses.complexchar('e\u0301')), 'e\u0301') | |
| # Defaults: no attributes, color pair 0. | |
| cc = curses.complexchar('z') | |
| self.assertEqual(str(cc), 'z') | |
| self.assertEqual(cc.attr, 0) | |
| self.assertEqual(cc.pair, 0) | |
| # attr never carries the color pair. | |
| self.assertEqual(curses.complexchar('A', 0, 1).attr, 0) | |
| self.assertEqual(curses.complexchar('A', curses.A_BOLD, 1).attr, | |
| curses.A_BOLD) | |
| self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) | |
| # Immutable rendition. | |
| self.assertRaises(AttributeError, setattr, cc, 'attr', 1) | |
| self.assertRaises(AttributeError, setattr, cc, 'pair', 1) | |
| # Equality and hashing compare text, attributes and color pair. | |
| self.assertEqual(curses.complexchar('A', curses.A_BOLD), | |
| curses.complexchar('A', curses.A_BOLD)) | |
| self.assertEqual(hash(curses.complexchar('A', curses.A_BOLD)), | |
| hash(curses.complexchar('A', curses.A_BOLD))) | |
| self.assertNotEqual(curses.complexchar('A'), | |
| curses.complexchar('A', curses.A_BOLD)) | |
| self.assertNotEqual(curses.complexchar('A'), curses.complexchar('B')) | |
| # repr() shows only a non-default attr/pair, and is a constructor call. | |
| modname = type(cc).__module__ | |
| ns = {modname: sys.modules[modname]} | |
| self.assertNotIn('attr=', repr(curses.complexchar('z'))) | |
| self.assertNotIn('pair=', repr(curses.complexchar('z'))) | |
| r = repr(curses.complexchar('A', curses.A_BOLD)) | |
| self.assertIn('attr=', r) | |
| self.assertNotIn('pair=', r) | |
| self.assertEqual(eval(r, ns), curses.complexchar('A', curses.A_BOLD)) | |
| # Invalid arguments. | |
| self.assertRaises(TypeError, curses.complexchar, 65) | |
| self.assertRaises(TypeError, curses.complexchar, 'A', 'bold') | |
| self.assertRaises(OverflowError, curses.complexchar, 'A', -1) | |
| self.assertRaises(OverflowError, curses.complexchar, 'A', 1 << 64) | |
| self.assertRaises(ValueError, curses.complexchar, 'A', 0, -1) | |
| self.assertRaises(ValueError, curses.complexchar, 'ab') | |
| def test_in_wch(self): | |
| # in_wch() returns the styled wide cell as a complexchar -- something | |
| # inch() (a packed chtype) cannot represent. | |
| stdscr = self.stdscr | |
| stdscr.addch(0, 0, curses.complexchar('A', curses.A_UNDERLINE)) | |
| cc = stdscr.in_wch(0, 0) | |
| self.assertEqual(str(cc), 'A') | |
| self.assertTrue(cc.attr & curses.A_UNDERLINE) | |
| # A character round-trips through the cell. See _encodable for the set. | |
| for ch in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): | |
| if self._storable(ch): | |
| with self.subTest(ch=ch): | |
| stdscr.addch(3, 0, curses.complexchar(ch)) | |
| self.assertEqual(str(stdscr.in_wch(3, 0)), ch) | |
| # in_wch() without coordinates reads at the cursor position. | |
| stdscr.move(0, 0) | |
| self.assertEqual(str(stdscr.in_wch()), 'A') | |
| @requires_colors | |
| def test_in_wch_color(self): | |
| # Unlike the chtype methods (which pack the pair into the value via | |
| # COLOR_PAIR), a complex character carries its color pair separately. | |
| stdscr = self.stdscr | |
| curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) | |
| stdscr.addch(0, 0, curses.complexchar('A', curses.A_BOLD, 1)) | |
| cc = stdscr.in_wch(0, 0) | |
| self.assertEqual(str(cc), 'A') | |
| self.assertEqual(cc.attr, curses.A_BOLD) | |
| self.assertEqual(cc.pair, 1) | |
| self.assertEqual(curses.complexchar('A', 0, 1).pair, 1) | |
| def test_getbkgrnd(self): | |
| # getbkgrnd() returns the background as a complexchar (getbkgd() can | |
| # only return a packed chtype). | |
| stdscr = self.stdscr | |
| stdscr.bkgdset(curses.complexchar(' ', curses.A_DIM)) | |
| stdscr.bkgd(curses.complexchar(' ', curses.A_BOLD)) | |
| cc = stdscr.getbkgrnd() | |
| self.assertEqual(str(cc), ' ') | |
| self.assertTrue(cc.attr & curses.A_BOLD) | |
| # A non-ASCII background round-trips as a complexchar. See _encodable. | |
| for ch in ('é', '¤', '€', 'є'): | |
| if self._storable(ch): | |
| with self.subTest(ch=ch): | |
| stdscr.bkgd(curses.complexchar(ch)) | |
| self.assertEqual(str(stdscr.getbkgrnd()), ch) | |
| stdscr.bkgd(' ') | |
| 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). | |
| cc = curses.complexchar | |
| B = curses.A_BOLD | |
| # Built from an iterable whose items are complexchar or str cells. | |
| s = curses.complexstr([cc('A', B), 'b', cc('c')]) | |
| self.assertEqual(len(s), 3) | |
| self.assertEqual(str(s), 'Abc') | |
| # Indexing yields a complexchar carrying the cell's rendition. | |
| self.assertIsInstance(s[0], curses.complexchar) | |
| self.assertEqual(str(s[0]), 'A') | |
| self.assertTrue(s[0].attr & B) | |
| self.assertEqual(s[-1], cc('c')) | |
| self.assertRaises(IndexError, lambda: s[3]) | |
| # Iteration walks the cells. | |
| self.assertEqual([str(c) for c in s], ['A', 'b', 'c']) | |
| # Slicing and concatenation produce new complexstr instances. | |
| self.assertIsInstance(s[1:], curses.complexstr) | |
| self.assertEqual(str(s[1:]), 'bc') | |
| self.assertEqual(str(s[::-1]), 'cbA') | |
| self.assertEqual(str(s + curses.complexstr(['Z'])), 'AbcZ') | |
| # Concatenating anything else raises instead of returning NotImplemented. | |
| self.assertRaises(TypeError, lambda: s + 'Z') | |
| self.assertRaises(TypeError, lambda: s + cc('Z')) | |
| # The empty complexstr. | |
| self.assertEqual(len(curses.complexstr([])), 0) | |
| self.assertEqual(str(curses.complexstr('')), '') | |
| # Equality and hashing compare the cells (text, attributes, pair). | |
| self.assertEqual(s, curses.complexstr([cc('A', B), 'b', cc('c')])) | |
| self.assertEqual(hash(s), | |
| hash(curses.complexstr([cc('A', B), 'b', cc('c')]))) | |
| self.assertNotEqual(s, curses.complexstr([cc('A'), 'b', cc('c')])) | |
| self.assertNotEqual(s, curses.complexstr([cc('A', B), 'b'])) | |
| # A spacing character optionally followed by combining characters. | |
| if self._storable('é'): | |
| self.assertEqual(str(curses.complexstr(['é', 'x'])), | |
| 'éx') | |
| # cells is positional-only. | |
| self.assertRaises(TypeError, lambda: curses.complexstr(cells=['x'])) | |
| # Invalid arguments. | |
| self.assertRaises(TypeError, curses.complexstr, 5) | |
| self.assertRaises(TypeError, curses.complexstr, [65]) | |
| self.assertRaises(ValueError, curses.complexstr, ['ab']) | |
| # A string is split into character cells, grouping each base character | |
| # with the combining characters that follow it (not one cell per code | |
| # point), unlike a generic sequence whose items are each one cell. | |
| self.assertEqual(len(curses.complexstr('abc')), 3) | |
| self.assertEqual(str(curses.complexstr('abc')), 'abc') | |
| self.assertEqual(len(curses.complexstr('')), 0) | |
| base = 'é' # 'e' + combining acute: two code points, one cell | |
| # Combining sequences need wide-character cells (a narrow build stores | |
| # one byte per cell). | |
| 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) | |
| # A combining character cannot begin a cell: one that leads the | |
| # string, or overflows a base's combining slots, has no base. | |
| self.assertRaises(ValueError, curses.complexstr, '\u0301') | |
| self.assertRaises(ValueError, curses.complexstr, 'e' + '\u0301' * 10) | |
| # A control character may stand alone but not carry combining marks. | |
| self.assertRaises(ValueError, curses.complexstr, '\n\u0301') | |
| # attr and pair apply to every cell of a string; pair is optional. | |
| styled = curses.complexstr('hi', B, 0) | |
| self.assertTrue(all(styled[i].attr & B for i in range(len(styled)))) | |
| self.assertEqual(curses.complexstr('x', B)[0], cc('x', B)) | |
| self.assertEqual(curses.complexstr('x', B, 0)[0], cc('x', B, 0)) | |
| # attr and pair may also be passed by keyword. | |
| self.assertEqual(curses.complexstr('x', attr=B)[0], cc('x', B)) | |
| self.assertEqual(curses.complexstr('x', attr=B, pair=0)[0], cc('x', B, 0)) | |
| self.assertEqual(curses.complexstr('x', pair=0)[0], cc('x', 0, 0)) | |
| # cells is positional-only. | |
| self.assertRaises(TypeError, lambda: curses.complexstr(cells='x')) | |
| self.assertRaises(ValueError, curses.complexstr, 'a', 0, -1) | |
| self.assertRaises(ValueError, lambda: curses.complexstr('a', pair=-1)) | |
| # For a non-string, giving attr/pair at all is an error (the cells | |
| # carry their own rendition) -- even attr=0. | |
| self.assertRaises(TypeError, curses.complexstr, [cc('A')], B) | |
| self.assertRaises(TypeError, curses.complexstr, [cc('A')], 0) | |
| self.assertRaises(TypeError, curses.complexstr, ['A'], 0, 0) | |
| self.assertRaises(TypeError, | |
| lambda: curses.complexstr([cc('A')], attr=B)) | |
| self.assertRaises(TypeError, | |
| lambda: curses.complexstr(['A'], pair=0)) | |
| 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. | |
| stdscr = self.stdscr | |
| cc = curses.complexchar | |
| B = curses.A_BOLD | |
| s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)]) | |
| stdscr.addstr(0, 0, s) | |
| r = stdscr.in_wchstr(0, 0, 3) | |
| self.assertIsInstance(r, curses.complexstr) | |
| # A read followed by a re-write is an exact round-trip. | |
| self.assertEqual(r, s) | |
| self.assertEqual(str(r), 'AbC') | |
| self.assertTrue(r[0].attr & B) | |
| self.assertFalse(r[1].attr & B) | |
| # The count is optional and reads to the end of the line by default. | |
| stdscr.move(0, 0) | |
| self.assertEqual(str(stdscr.in_wchstr())[:3], 'AbC') | |
| self.assertEqual(stdscr.in_wchstr(0, 0, 2**31), stdscr.in_wchstr(0, 0)) | |
| self.assertRaises(OverflowError, stdscr.in_wchstr, 2**1000) | |
| self.assertRaises(ValueError, stdscr.in_wchstr, -2) | |
| self.assertRaises(ValueError, stdscr.in_wchstr, 0, 2, -2) | |
| self.assertRaises(ValueError, stdscr.in_wchstr, -2**1000) | |
| 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. | |
| stdscr = self.stdscr | |
| cc = curses.complexchar | |
| B = curses.A_BOLD | |
| s = curses.complexstr([cc('A', B), cc('b'), cc('C', B)]) | |
| # addstr with a complexstr round-trips. | |
| stdscr.addstr(0, 0, s) | |
| self.assertEqual(stdscr.in_wchstr(0, 0, 3), s) | |
| # addnstr writes at most n cells. | |
| stdscr.addstr(2, 0, '....') | |
| stdscr.addnstr(2, 0, s, 2) | |
| self.assertEqual(str(stdscr.in_wchstr(2, 0, 4)), 'Ab..') | |
| # insstr inserts the cells in order. | |
| stdscr.move(3, 0) | |
| stdscr.addstr('END') | |
| stdscr.insstr(3, 0, curses.complexstr([cc('P'), cc('Q')])) | |
| self.assertEqual(str(stdscr.in_wchstr(3, 0, 5)), 'PQEND') | |
| # insnstr inserts at most n cells. | |
| stdscr.move(4, 0) | |
| stdscr.addstr('END') | |
| stdscr.insnstr(4, 0, curses.complexstr(['1', '2', '3']), 2) | |
| self.assertEqual(str(stdscr.in_wchstr(4, 0, 5)), '12END') | |
| # An empty run is accepted (and still honours the move). | |
| stdscr.addstr(5, 0, curses.complexstr([])) | |
| stdscr.insstr(5, 0, curses.complexstr([])) | |
| # Cells carry their own rendition, so an explicit attr is rejected. | |
| self.assertRaises(TypeError, stdscr.addstr, s, B) | |
| self.assertRaises(TypeError, stdscr.addnstr, s, 2, B) | |
| self.assertRaises(TypeError, stdscr.insstr, s, B) | |
| self.assertRaises(TypeError, stdscr.insnstr, s, 2, B) | |
| # A bare sequence of cells is not accepted; build a complexstr first. | |
| self.assertRaises(TypeError, stdscr.addstr, [cc('A'), 'b']) | |
| self.assertRaises(TypeError, stdscr.insstr, [cc('A'), 'b']) | |
| def test_output_character(self): | |
| stdscr = self.stdscr | |
| encoding = stdscr.encoding | |
| # addch() | |
| stdscr.refresh() | |
| stdscr.move(0, 0) | |
| stdscr.addch('A') | |
| stdscr.addch(b'A') | |
| stdscr.addch(65) | |
| # See _encodable for the character set. Each is either written (mapped | |
| # to a single byte), or raises UnicodeEncodeError (not in the encoding) | |
| # or OverflowError (a multibyte sequence, e.g. in UTF-8). | |
| for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): | |
| try: | |
| stdscr.addch(c) | |
| except UnicodeEncodeError: | |
| self.assertRaises(UnicodeEncodeError, c.encode, encoding) | |
| except OverflowError: | |
| encoded = c.encode(encoding) | |
| self.assertNotEqual(len(encoded), 1, repr(encoded)) | |
| stdscr.addch('A', curses.A_BOLD) | |
| stdscr.addch(1, 2, 'A') | |
| stdscr.addch(2, 3, 'A', curses.A_BOLD) | |
| self.assertIs(stdscr.is_wintouched(), True) | |
| # The same characters supplied as an int chtype. The cell is read back | |
| # with _read_char(), not inch(): on a wide build the int is stored as a | |
| # wide character that inch() cannot represent for a character outside | |
| # Latin-1. The int is decoded as a locale byte, so only a single-byte | |
| # character round-trips. | |
| for c in ('é', '¤', '€', 'є'): | |
| v = self._char_code(c) | |
| if v is None: | |
| continue | |
| with self.subTest(c=c): | |
| stdscr.addch(0, 0, v) | |
| self.assertEqual(self._read_char(0, 0), c) | |
| stdscr.addch(0, 1, v, curses.A_BOLD) | |
| self.assertEqual(self._read_char(0, 1), c) | |
| self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) | |
| stdscr.move(2, 0) | |
| stdscr.echochar(v) | |
| self.assertEqual(self._read_char(2, 0), c) | |
| # insch() decodes the byte through the locale like addch(), so | |
| # it round-trips the same character. | |
| stdscr.insch(1, 0, v) | |
| self.assertEqual(self._read_char(1, 0), c) | |
| # The same characters supplied as a str. Unlike the int path above, a | |
| # 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. | |
| for c in ('é', '¤', '€', 'є'): | |
| if not self._storable(c): | |
| continue | |
| with self.subTest(c=c): | |
| stdscr.addch(0, 0, c) | |
| self.assertEqual(self._read_char(0, 0), c) | |
| stdscr.addch(0, 1, c, curses.A_BOLD) | |
| self.assertEqual(self._read_char(0, 1), c) | |
| self.assertTrue(stdscr.inch(0, 1) & curses.A_BOLD) | |
| stdscr.insch(1, 0, c) | |
| self.assertEqual(self._read_char(1, 0), c) | |
| stdscr.move(2, 0) | |
| stdscr.echochar(c) | |
| self.assertEqual(self._read_char(2, 0), c) | |
| # echochar() | |
| stdscr.refresh() | |
| stdscr.move(0, 0) | |
| stdscr.echochar('A') | |
| stdscr.echochar(b'A') | |
| stdscr.echochar(65) | |
| # See _encodable for the character set; as in the addch() loop above. | |
| for c in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): | |
| try: | |
| stdscr.echochar(c) | |
| except UnicodeEncodeError: | |
| # The character is not encodable with the current encoding. | |
| self.assertRaises(UnicodeEncodeError, c.encode, encoding) | |
| except OverflowError: | |
| # The character is encoded to a multibyte sequence. | |
| encoded = c.encode(encoding) | |
| self.assertNotEqual(len(encoded), 1, repr(encoded)) | |
| stdscr.echochar('A', curses.A_BOLD) | |
| self.assertIs(stdscr.is_wintouched(), False) | |
| def test_output_string(self): | |
| stdscr = self.stdscr | |
| encoding = stdscr.encoding | |
| # addstr()/insstr() | |
| for func in [stdscr.addstr, stdscr.insstr]: | |
| with self.subTest(func.__qualname__): | |
| func('abcd') | |
| func(b'abcd') | |
| # Common and encoding-distinctive strings (see _encodable for the | |
| # 0xA4 set); 'àßçđ' is UTF-8-only. Each is written if the | |
| # encoding allows, else raises UnicodeEncodeError. | |
| for s in ('soupçon', 'àßçđ', 'soupçon ¤', 'soupçon €', 'дякую'): | |
| stdscr.move(0, 0) | |
| try: | |
| func(s) | |
| except UnicodeEncodeError: | |
| self.assertRaises(UnicodeEncodeError, s.encode, encoding) | |
| stdscr.move(0, 0) | |
| func('abcd', curses.A_BOLD) | |
| func(1, 2, 'abcd') | |
| func(2, 3, 'abcd', curses.A_BOLD) | |
| # addnstr()/insnstr() | |
| for func in [stdscr.addnstr, stdscr.insnstr]: | |
| with self.subTest(func.__qualname__): | |
| stdscr.move(0, 0) | |
| func('1234', 3) | |
| func(b'1234', 3) | |
| # As above (see _encodable); Arabic-Indic digits are UTF-8-only. | |
| for s in ('caf\u00e9', '\u0661\u0662\u0663\u0664', 'caf\u00e9 \u00a4', 'caf\u00e9 \u20ac', '\u0434\u044f\u043a\u0443\u044e'): | |
| stdscr.move(0, 0) | |
| try: | |
| func(s, 3) | |
| except UnicodeEncodeError: | |
| self.assertRaises(UnicodeEncodeError, s.encode, encoding) | |
| stdscr.move(0, 0) | |
| func('1234', 5) | |
| func('1234', 3, curses.A_BOLD) | |
| func(1, 2, '1234', 3) | |
| func(2, 3, '1234', 3, curses.A_BOLD) | |
| def test_output_string_embedded_null_chars(self): | |
| # reject embedded null bytes and characters | |
| stdscr = self.stdscr | |
| for arg in ['a\0', b'a\0']: | |
| with self.subTest(arg=arg): | |
| self.assertRaises(ValueError, stdscr.addstr, arg) | |
| self.assertRaises(ValueError, stdscr.addnstr, arg, 1) | |
| self.assertRaises(ValueError, stdscr.insstr, arg) | |
| self.assertRaises(ValueError, stdscr.insnstr, arg, 1) | |
| def test_output_string_attr_restored(self): | |
| # A write with an attr restores the window rendition afterwards, | |
| # whether it succeeded or failed. | |
| win = curses.newwin(2, 10, 0, 0) | |
| for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)), | |
| (win.insstr, ('x',)), (win.insnstr, ('x', 1))]: | |
| with self.subTest(func.__qualname__): | |
| win.attrset(curses.A_UNDERLINE) | |
| # y=100 is outside the window, so the write fails. | |
| self.assertRaises(curses.error, func, 100, 0, *args, | |
| curses.A_BOLD) | |
| self.assertEqual(win.getattrs(), curses.A_UNDERLINE) | |
| func(0, 0, *args, curses.A_BOLD) | |
| self.assertEqual(win.getattrs(), curses.A_UNDERLINE) | |
| @requires_colors | |
| @requires_curses_window_meth('color_set') | |
| @requires_curses_window_meth('attr_get') | |
| def test_output_string_pair_restored(self): | |
| # The rendition put back after a write includes the color pair, also | |
| # when it is larger than the A_COLOR field of a chtype holds. | |
| pairs = [7] | |
| if curses.has_extended_color_support() and curses.COLOR_PAIRS > 300: | |
| pairs.append(300) | |
| win = curses.newwin(2, 10, 0, 0) | |
| for pair in pairs: | |
| curses.init_pair(pair, curses.COLOR_RED, curses.COLOR_BLACK) | |
| for func, args in [(win.addstr, ('x',)), (win.addnstr, ('x', 1)), | |
| (win.insstr, ('x',)), (win.insnstr, ('x', 1))]: | |
| with self.subTest(func.__qualname__, pair=pair): | |
| win.color_set(pair) | |
| func(0, 0, *args, curses.A_BOLD) | |
| self.assertEqual(win.attr_get()[1], pair) | |
| win.color_set(pair) | |
| # y=100 is outside the window, so the write fails. | |
| self.assertRaises(curses.error, func, 100, 0, *args, | |
| curses.A_BOLD) | |
| self.assertEqual(win.attr_get()[1], pair) | |
| def test_cell_embedded_null_chars(self): | |
| # A NUL cannot share a cell with another character: setcchar() takes a | |
| # NUL-terminated string, so the rest of the cell would be dropped. | |
| for text in ['a\0', 'a\0\u0301', 'a\0b', '\0a']: | |
| with self.subTest(text=text): | |
| self.assertRaises(ValueError, curses.complexchar, text) | |
| if WIDE_BUILD: | |
| self.assertRaises(ValueError, self.stdscr.addch, text) | |
| def test_cell_null_char(self): | |
| # A lone NUL is a character like any other, as addch(0) always was. | |
| stdscr = self.stdscr | |
| cell = curses.complexchar('\0') | |
| self.assertEqual(str(cell), '\0') | |
| self.assertEqual(eval(repr(cell), {'curses': curses}), cell) | |
| stdscr.erase() | |
| stdscr.addch(0, 0, 0) | |
| expected = stdscr.instr(0, 0, 4) | |
| for ch in ['\0', cell]: | |
| with self.subTest(ch=ch): | |
| stdscr.erase() | |
| stdscr.addch(0, 0, ch) | |
| self.assertEqual(stdscr.instr(0, 0, 4), expected) | |
| # A cell holding a NUL reads back as the cell that writes it. | |
| win = curses.newwin(3, 8, 0, 0) | |
| win.insch(0, 0, '\0') | |
| if WIDE_BUILD: | |
| self.assertEqual(win.in_wch(0, 0), cell) | |
| else: | |
| # A narrow build inserts a NUL as "^@". | |
| self.assertEqual(str(win.in_wch(0, 0)), '^') | |
| # A string of cells cannot hold a NUL: it would end a batch write. | |
| self.assertRaises(ValueError, curses.complexstr, 'a\0b') | |
| self.assertRaises(ValueError, curses.complexstr, '\0') | |
| def test_add_string_behavior(self): | |
| # addstr() advances the cursor past the written text; addnstr() | |
| # writes at most n characters. | |
| win = curses.newwin(1, 10, 0, 0) | |
| win.addstr(0, 0, 'abc') | |
| self.assertEqual(win.getyx(), (0, 3)) | |
| win.erase() | |
| win.addnstr(0, 0, 'abcdef', 3) | |
| self.assertEqual(win.instr(0, 0), b'abc ') | |
| def test_insert_string_behavior(self): | |
| # insstr()/insnstr() insert at the cursor, shift the rest of the | |
| # line right (losing characters off the edge), and leave the cursor | |
| # where it was. | |
| win = curses.newwin(1, 10, 0, 0) | |
| win.addstr(0, 0, 'abcde') | |
| win.move(0, 1) | |
| win.insstr('XY') | |
| self.assertEqual(win.getyx(), (0, 1)) # cursor did not advance | |
| self.assertEqual(win.instr(0, 0), b'aXYbcde ') | |
| win.erase() | |
| win.addstr(0, 0, 'ZZZZZ') | |
| win.move(0, 0) | |
| win.insnstr('abcdef', 3) # at most 3 characters | |
| self.assertEqual(win.instr(0, 0), b'abcZZZZZ ') | |
| def test_insch(self): | |
| # insch() inserts a single character at the cursor (or at y, x), | |
| # shifting the rest of the line right. | |
| win = curses.newwin(2, 10, 0, 0) | |
| win.addstr(0, 0, 'abc') | |
| win.move(0, 1) | |
| win.insch(ord('X')) | |
| self.assertEqual(win.instr(0, 0), b'aXbc ') | |
| win.insch(1, 0, 'Y', curses.A_BOLD) | |
| self.assertEqual(win.inch(1, 0), b'Y'[0] | curses.A_BOLD) | |
| def test_pad(self): | |
| pad = curses.newpad(10, 20) | |
| pad.addstr(0, 0, 'PADTEXT') | |
| self.assertEqual(pad.instr(0, 0, 7), b'PADTEXT') | |
| # subpad() creates a pad within the parent pad. Cell sharing with | |
| # the parent is implementation-defined, so write to the subpad itself. | |
| sub = pad.subpad(3, 5, 0, 0) | |
| self.assertEqual(sub.getmaxyx(), (3, 5)) | |
| sub.addstr(1, 0, 'sub') | |
| self.assertEqual(sub.instr(1, 0, 3), b'sub') | |
| # A pad is refreshed onto an explicit screen rectangle; the | |
| # 6-argument form is required (and rejected for ordinary windows). | |
| pad.refresh(0, 0, 0, 0, 4, 10) | |
| pad.noutrefresh(0, 0, 0, 0, 4, 10) | |
| curses.doupdate() | |
| self.assertRaises(TypeError, pad.refresh) | |
| win = curses.newwin(5, 5, 0, 0) | |
| self.assertRaises(TypeError, win.refresh, 0, 0, 0, 0, 4, 4) | |
| def test_read_from_window(self): | |
| stdscr = self.stdscr | |
| stdscr.addstr(0, 1, 'ABCD', curses.A_BOLD) | |
| # inch() | |
| stdscr.move(0, 1) | |
| self.assertEqual(stdscr.inch(), 65 | curses.A_BOLD) | |
| self.assertEqual(stdscr.inch(0, 3), 67 | curses.A_BOLD) | |
| stdscr.move(0, 0) | |
| # instr() | |
| self.assertEqual(stdscr.instr()[:6], b' ABCD ') | |
| self.assertEqual(stdscr.instr(3)[:6], b' AB') | |
| self.assertEqual(stdscr.instr(0, 2)[:4], b'BCD ') | |
| self.assertEqual(stdscr.instr(0, 2, 4), b'BCD ') | |
| # A huge count is bounded by the line, and is not used to size the | |
| # read buffer. | |
| self.assertEqual(stdscr.instr(0, 0, 2**31), stdscr.instr(0, 0)) | |
| self.assertRaises(OverflowError, stdscr.instr, 2**1000) | |
| self.assertRaises(ValueError, stdscr.instr, -2) | |
| self.assertRaises(ValueError, stdscr.instr, 0, 2, -2) | |
| self.assertRaises(ValueError, stdscr.instr, -2**1000) | |
| # instr(y, x, 1) reads a single cell byte, so only a character that the | |
| # window encoding maps to one byte is checked. inch() returns the cell | |
| # value, which is the locale byte. | |
| for ch in ('A', 'é', '¤', '€', 'є'): | |
| try: | |
| b = ch.encode(stdscr.encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(b) != 1: | |
| continue | |
| v = self._char_code(ch) | |
| with self.subTest(ch=ch): | |
| stdscr.addstr(2, 0, ch) | |
| self.assertEqual(stdscr.instr(2, 0, 1), b) | |
| self.assertEqual(stdscr.inch(2, 0), v) | |
| def test_read_long_line(self): | |
| # A pad line can be longer than a window, and a character can be | |
| # encoded with several bytes, so instr() can read more bytes than | |
| # there are cells. See _encodable for the character set. | |
| width = 3000 | |
| pad = curses.newpad(1, width) | |
| for ch in ['z', '\u00e9', '\u20ac', '\u0434', '\uff71']: | |
| if not self._storable(ch): | |
| continue | |
| pad.addstr(0, 0, ch) | |
| if pad.getyx()[1] != 1: | |
| continue # a wide character occupies two cells | |
| with self.subTest(ch=ch): | |
| line = ch * (width - 1) + ' ' # the last cell is left blank | |
| pad.addstr(0, 0, line[:-1]) | |
| self.assertEqual(pad.instr(0, 0), line.encode(pad.encoding)) | |
| self.assertEqual(pad.in_wstr(0, 0), line) | |
| self.assertEqual(str(pad.in_wchstr(0, 0)), line) | |
| def test_coordinate_errors(self): | |
| # Addressing a cell outside the window raises curses.error. | |
| win = curses.newwin(5, 10, 0, 0) | |
| self.assertRaises(curses.error, win.move, 100, 100) | |
| self.assertRaises(curses.error, win.move, -1, -1) | |
| self.assertRaises(curses.error, win.addch, 100, 100, ord('x')) | |
| self.assertRaises(curses.error, win.inch, 100, 100) | |
| if hasattr(win, 'chgat'): # chgat() requires wchgat() | |
| self.assertRaises(curses.error, win.chgat, 100, 0, curses.A_BOLD) | |
| def test_argument_errors(self): | |
| win = curses.newwin(5, 10, 0, 0) | |
| # A character argument must be an int, a byte or a one-element string. | |
| self.assertRaises(TypeError, win.addch, []) | |
| self.assertRaises(OverflowError, win.addch, 2**64) | |
| # The attribute argument is rejected, not truncated, when out of range. | |
| self.assertRaises(OverflowError, win.addch, 'a', 2**64) | |
| self.assertRaises(OverflowError, win.addstr, 'a', 2**64) | |
| self.assertRaises(TypeError, win.addch, 'a', 'bold') | |
| # A string method rejects a non-string, non-bytes argument. | |
| self.assertRaises(TypeError, win.addstr, 5) | |
| self.assertRaises(TypeError, win.addstr) | |
| # Wrong number of positional arguments. | |
| self.assertRaises(TypeError, win.instr, 0, 0, 0, 0) | |
| def test_getch(self): | |
| win = curses.newwin(5, 12, 5, 2) | |
| # TODO: Test with real input by writing to master fd. | |
| for c in 'spam\n'[::-1]: | |
| curses.ungetch(c) | |
| self.assertEqual(win.getch(3, 1), b's'[0]) | |
| self.assertEqual(win.getyx(), (3, 1)) | |
| self.assertEqual(win.getch(3, 4), b'p'[0]) | |
| self.assertEqual(win.getyx(), (3, 4)) | |
| self.assertEqual(win.getch(), b'a'[0]) | |
| self.assertEqual(win.getyx(), (3, 4)) | |
| self.assertEqual(win.getch(), b'm'[0]) | |
| self.assertEqual(win.getch(), b'\n'[0]) | |
| # A non-ASCII character encodable as a single byte in the locale | |
| # round-trips as that byte. | |
| encoding = self.stdscr.encoding | |
| for ch in ('é', '¤', '€', 'є'): | |
| try: | |
| b = ch.encode(encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(b) != 1: | |
| continue | |
| with self.subTest(ch=ch): | |
| curses.ungetch(self._char_code(ch)) | |
| self.assertEqual(win.getch(), b[0]) | |
| # A key code is delivered unchanged. | |
| curses.ungetch(curses.KEY_LEFT) | |
| self.assertEqual(win.getch(), curses.KEY_LEFT) | |
| def test_getstr(self): | |
| win = curses.newwin(5, 12, 5, 2) | |
| curses.echo() | |
| self.addCleanup(curses.noecho) | |
| self.assertRaises(ValueError, win.getstr, -400) | |
| self.assertRaises(ValueError, win.getstr, 2, 3, -400) | |
| # TODO: Test with real input by writing to master fd. | |
| for c in 'Lorem\nipsum\ndolor\nsit\namet\n'[::-1]: | |
| curses.ungetch(c) | |
| self.assertEqual(win.getstr(3, 1, 2), b'Lo') | |
| self.assertEqual(win.instr(3, 0), b' Lo ') | |
| self.assertEqual(win.getstr(3, 5, 10), b'ipsum') | |
| self.assertEqual(win.instr(3, 0), b' Lo ipsum ') | |
| self.assertEqual(win.getstr(1, 5), b'dolor') | |
| self.assertEqual(win.instr(1, 0), b' dolor ') | |
| self.assertEqual(win.getstr(2), b'si') | |
| self.assertEqual(win.instr(1, 0), b'si dolor ') | |
| self.assertEqual(win.getstr(), b'amet') | |
| self.assertEqual(win.instr(1, 0), b'amet dolor ') | |
| 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. | |
| win = curses.newwin(5, 12, 5, 2) | |
| curses.echo() | |
| self.addCleanup(curses.noecho) | |
| for s in ['Lorem', # ASCII | |
| 'naïve', # common to the Latin encodings | |
| 'naïve ¤', # ISO-8859-1 | |
| 'soupçon €', # ISO-8859-15 | |
| 'дяк']: # KOI8-U | |
| if self._storable(s): | |
| with self.subTest(s=s): | |
| win.erase() | |
| for ch in reversed(s + '\n'): | |
| curses.unget_wch(ch) | |
| self.assertEqual(win.get_wstr(0, 0), s) | |
| def test_clear(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| lorem_ipsum(win) | |
| win.move(0, 8) | |
| win.clrtoeol() | |
| self.assertEqual(win.instr(0, 0).rstrip(), b'Lorem ip') | |
| self.assertEqual(win.instr(1, 0).rstrip(), b'dolor sit amet,') | |
| win.move(0, 3) | |
| win.clrtobot() | |
| self.assertEqual(win.instr(0, 0).rstrip(), b'Lor') | |
| self.assertEqual(win.instr(1, 0).rstrip(), b'') | |
| for func in [win.erase, win.clear]: | |
| lorem_ipsum(win) | |
| func() | |
| self.assertEqual(win.instr(0, 0).rstrip(), b'') | |
| self.assertEqual(win.instr(1, 0).rstrip(), b'') | |
| def test_insert_delete(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| lorem_ipsum(win) | |
| win.move(0, 2) | |
| win.delch() | |
| self.assertEqual(win.instr(0, 0), b'Loem ipsum ') | |
| win.delch(0, 7) | |
| self.assertEqual(win.instr(0, 0), b'Loem ipum ') | |
| win.move(1, 5) | |
| win.deleteln() | |
| self.assertEqual(win.instr(0, 0), b'Loem ipum ') | |
| self.assertEqual(win.instr(1, 0), b'consectetur ') | |
| self.assertEqual(win.instr(2, 0), b'adipiscing elit') | |
| self.assertEqual(win.instr(3, 0), b'sed do eiusmod ') | |
| self.assertEqual(win.instr(4, 0), b' ') | |
| win.move(1, 5) | |
| win.insertln() | |
| self.assertEqual(win.instr(0, 0), b'Loem ipum ') | |
| self.assertEqual(win.instr(1, 0), b' ') | |
| self.assertEqual(win.instr(2, 0), b'consectetur ') | |
| win.clear() | |
| lorem_ipsum(win) | |
| win.move(1, 5) | |
| win.insdelln(2) | |
| self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') | |
| self.assertEqual(win.instr(1, 0), b' ') | |
| self.assertEqual(win.instr(2, 0), b' ') | |
| self.assertEqual(win.instr(3, 0), b'dolor sit amet,') | |
| win.clear() | |
| lorem_ipsum(win) | |
| win.move(1, 5) | |
| win.insdelln(-2) | |
| self.assertEqual(win.instr(0, 0), b'Lorem ipsum ') | |
| self.assertEqual(win.instr(1, 0), b'adipiscing elit') | |
| self.assertEqual(win.instr(2, 0), b'sed do eiusmod ') | |
| self.assertEqual(win.instr(3, 0), b' ') | |
| def test_scroll(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| lorem_ipsum(win) | |
| win.scrollok(True) | |
| win.scroll() | |
| self.assertEqual(win.instr(0, 0), b'dolor sit amet,') | |
| win.scroll(2) | |
| self.assertEqual(win.instr(0, 0), b'adipiscing elit') | |
| win.scroll(-3) | |
| self.assertEqual(win.instr(0, 0), b' ') | |
| self.assertEqual(win.instr(2, 0), b' ') | |
| self.assertEqual(win.instr(3, 0), b'adipiscing elit') | |
| win.scrollok(False) | |
| def test_attributes(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| win.attron(curses.A_BOLD) | |
| win.attroff(curses.A_BOLD) | |
| win.attrset(curses.A_BOLD) | |
| win.standout() | |
| win.standend() | |
| # attron()/attroff()/attrset() reject a bad attribute. | |
| self.assertRaises(OverflowError, win.attron, 1 << 64) | |
| self.assertRaises(OverflowError, win.attroff, -1) | |
| self.assertRaises(OverflowError, win.attrset, 1 << 64) | |
| self.assertRaises(TypeError, win.attron, 'x') | |
| @requires_curses_window_meth('attr_set') | |
| def test_attr(self): | |
| # The attr_*() family works on attr_t attributes paired with a color | |
| # pair, unlike the chtype-based attron()/attroff()/attrset(). | |
| win = curses.newwin(5, 15, 5, 2) | |
| win.attr_set(curses.A_BOLD | curses.A_UNDERLINE) | |
| attrs, pair = win.attr_get() | |
| self.assertTrue(attrs & curses.A_BOLD) | |
| self.assertTrue(attrs & curses.A_UNDERLINE) | |
| self.assertEqual(pair, 0) | |
| self.assertEqual(win.getattrs(), attrs) | |
| win.attr_on(curses.A_REVERSE) | |
| self.assertTrue(win.attr_get()[0] & curses.A_REVERSE) | |
| win.attr_off(curses.A_REVERSE) | |
| self.assertFalse(win.attr_get()[0] & curses.A_REVERSE) | |
| # color_set() with a real pair needs start_color(); see | |
| # test_attr_color_pair. Here only the argument validation is checked, | |
| # which fails before wcolor_set() is reached. | |
| self.assertRaises(TypeError, win.attr_set, 'x') | |
| self.assertRaises(TypeError, win.attr_set, curses.A_BOLD, 'x') | |
| self.assertRaises(TypeError, win.attr_on, 'x') | |
| self.assertRaises(TypeError, win.color_set, 'x') | |
| self.assertRaises(ValueError, win.color_set, -1) | |
| self.assertRaises(ValueError, win.attr_set, curses.A_BOLD, -1) | |
| # attr_t is unsigned: a negative or too-large attribute overflows. | |
| self.assertRaises(OverflowError, win.attr_set, -1) | |
| self.assertRaises(OverflowError, win.attr_on, -1) | |
| self.assertRaises(OverflowError, win.attr_set, 1 << 64) | |
| @requires_colors | |
| @requires_curses_window_meth('attr_set') | |
| def test_attr_color_pair(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) | |
| win.attr_set(curses.A_BOLD, 1) | |
| attrs, pair = win.attr_get() | |
| self.assertTrue(attrs & curses.A_BOLD) | |
| self.assertEqual(pair, 1) | |
| win.color_set(0) | |
| self.assertEqual(win.attr_get()[1], 0) | |
| @requires_curses_window_meth('chgat') | |
| def test_chgat(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| win.addstr(2, 0, 'Lorem ipsum') | |
| win.addstr(3, 0, 'dolor sit amet') | |
| win.move(2, 8) | |
| win.chgat(curses.A_BLINK) | |
| self.assertEqual(win.inch(2, 7), b'p'[0]) | |
| self.assertEqual(win.inch(2, 8), b's'[0] | curses.A_BLINK) | |
| self.assertEqual(win.inch(2, 14), b' '[0] | curses.A_BLINK) | |
| win.move(2, 1) | |
| win.chgat(3, curses.A_BOLD) | |
| self.assertEqual(win.inch(2, 0), b'L'[0]) | |
| self.assertEqual(win.inch(2, 1), b'o'[0] | curses.A_BOLD) | |
| self.assertEqual(win.inch(2, 3), b'e'[0] | curses.A_BOLD) | |
| self.assertEqual(win.inch(2, 4), b'm'[0]) | |
| win.chgat(3, 2, curses.A_UNDERLINE) | |
| self.assertEqual(win.inch(3, 1), b'o'[0]) | |
| self.assertEqual(win.inch(3, 2), b'l'[0] | curses.A_UNDERLINE) | |
| self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) | |
| win.chgat(3, 4, 7, curses.A_BLINK) | |
| self.assertEqual(win.inch(3, 3), b'o'[0] | curses.A_UNDERLINE) | |
| self.assertEqual(win.inch(3, 4), b'r'[0] | curses.A_BLINK) | |
| self.assertEqual(win.inch(3, 10), b'a'[0] | curses.A_BLINK) | |
| self.assertEqual(win.inch(3, 11), b'm'[0] | curses.A_UNDERLINE) | |
| self.assertEqual(win.inch(3, 14), b' '[0] | curses.A_UNDERLINE) | |
| # attr_t is unsigned: a negative or too-large attribute overflows. | |
| self.assertRaises(TypeError, win.chgat, 'x') | |
| self.assertRaises(OverflowError, win.chgat, -1) | |
| self.assertRaises(OverflowError, win.chgat, 1 << 64) | |
| def test_background(self): | |
| win = curses.newwin(5, 15, 5, 2) | |
| win.addstr(0, 0, 'Lorem ipsum') | |
| self.assertIn(win.getbkgd(), (0, 32)) | |
| # bkgdset() | |
| win.bkgdset('_') | |
| self.assertEqual(win.getbkgd(), b'_'[0]) | |
| win.bkgdset(b'#') | |
| self.assertEqual(win.getbkgd(), b'#'[0]) | |
| win.bkgdset(65) | |
| self.assertEqual(win.getbkgd(), 65) | |
| win.bkgdset(0) | |
| self.assertEqual(win.getbkgd(), 32) | |
| win.bkgdset('#', curses.A_REVERSE) | |
| self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) | |
| self.assertEqual(win.inch(0, 0), b'L'[0]) | |
| self.assertEqual(win.inch(0, 5), b' '[0]) | |
| win.bkgdset(0) | |
| # bkgd() | |
| win.bkgd('_') | |
| self.assertEqual(win.getbkgd(), b'_'[0]) | |
| self.assertEqual(win.inch(0, 0), b'L'[0]) | |
| self.assertEqual(win.inch(0, 5), b'_'[0]) | |
| win.bkgd('#', curses.A_REVERSE) | |
| self.assertEqual(win.getbkgd(), b'#'[0] | curses.A_REVERSE) | |
| self.assertEqual(win.inch(0, 0), b'L'[0] | curses.A_REVERSE) | |
| self.assertEqual(win.inch(0, 5), b'#'[0] | curses.A_REVERSE) | |
| # A non-ASCII background character reads back as its cell value, the | |
| # locale byte. | |
| win.bkgd(' ') | |
| for ch in ('é', '¤', '€', 'є'): | |
| v = self._char_code(ch) | |
| if v is None: | |
| continue | |
| with self.subTest(ch=ch): | |
| win.bkgd(ch) | |
| self.assertEqual(win.getbkgd(), v) | |
| if ord(ch) < 0x100: | |
| # The same byte given as an int. A wide build stores it | |
| # through the locale, so only a Latin-1 byte round-trips. | |
| win.bkgd(' ') | |
| win.bkgdset(v) | |
| self.assertEqual(win.getbkgd(), v) | |
| win.bkgd(v) | |
| self.assertEqual(win.getbkgd(), v) | |
| def test_overlay(self): | |
| srcwin = curses.newwin(5, 18, 3, 4) | |
| lorem_ipsum(srcwin) | |
| dstwin = curses.newwin(7, 17, 5, 7) | |
| for i in range(6): | |
| dstwin.addstr(i, 0, '_'*17) | |
| srcwin.overlay(dstwin) | |
| self.assertEqual(dstwin.instr(0, 0), b'sectetur_________') | |
| self.assertEqual(dstwin.instr(1, 0), b'piscing_elit,____') | |
| self.assertEqual(dstwin.instr(2, 0), b'_do_eiusmod______') | |
| self.assertEqual(dstwin.instr(3, 0), b'_________________') | |
| srcwin.overwrite(dstwin) | |
| self.assertEqual(dstwin.instr(0, 0), b'sectetur __') | |
| self.assertEqual(dstwin.instr(1, 0), b'piscing elit, __') | |
| self.assertEqual(dstwin.instr(2, 0), b' do eiusmod __') | |
| self.assertEqual(dstwin.instr(3, 0), b'_________________') | |
| srcwin.overlay(dstwin, 1, 4, 3, 2, 4, 11) | |
| self.assertEqual(dstwin.instr(3, 0), b'__r_sit_amet_____') | |
| self.assertEqual(dstwin.instr(4, 0), b'__ectetur________') | |
| self.assertEqual(dstwin.instr(5, 0), b'_________________') | |
| srcwin.overwrite(dstwin, 1, 4, 3, 2, 4, 11) | |
| self.assertEqual(dstwin.instr(3, 0), b'__r sit amet_____') | |
| self.assertEqual(dstwin.instr(4, 0), b'__ectetur _____') | |
| self.assertEqual(dstwin.instr(5, 0), b'_________________') | |
| def test_refresh(self): | |
| win = curses.newwin(5, 15, 2, 5) | |
| win.noutrefresh() | |
| win.redrawln(1, 2) | |
| win.redrawwin() | |
| win.refresh() | |
| curses.doupdate() | |
| @requires_curses_window_meth('resize') | |
| def test_resize(self): | |
| win = curses.newwin(5, 15, 2, 5) | |
| win.resize(4, 20) | |
| self.assertEqual(win.getmaxyx(), (4, 20)) | |
| win.resize(5, 15) | |
| self.assertEqual(win.getmaxyx(), (5, 15)) | |
| @requires_curses_window_meth('enclose') | |
| def test_enclose(self): | |
| win = curses.newwin(5, 15, 2, 5) | |
| self.assertIs(win.enclose(2, 5), True) | |
| self.assertIs(win.enclose(1, 5), False) | |
| self.assertIs(win.enclose(2, 4), False) | |
| self.assertIs(win.enclose(6, 19), True) | |
| self.assertIs(win.enclose(7, 19), False) | |
| self.assertIs(win.enclose(6, 20), False) | |
| @requires_curses_window_meth('mouse_trafo') | |
| def test_mouse_trafo(self): | |
| win = curses.newwin(5, 15, 2, 5) | |
| # to_screen=True: window-relative -> stdscr-relative. | |
| self.assertEqual(win.mouse_trafo(0, 0, True), (2, 5)) | |
| self.assertEqual(win.mouse_trafo(3, 10, True), (5, 15)) | |
| self.assertEqual(win.mouse_trafo(4, 14, True), (6, 19)) | |
| # A coordinate outside the window has no counterpart. | |
| self.assertIsNone(win.mouse_trafo(5, 0, True)) | |
| self.assertIsNone(win.mouse_trafo(0, 15, True)) | |
| # to_screen=False is the inverse: stdscr-relative -> window-relative. | |
| self.assertEqual(win.mouse_trafo(2, 5, False), (0, 0)) | |
| self.assertEqual(win.mouse_trafo(6, 19, False), (4, 14)) | |
| self.assertIsNone(win.mouse_trafo(1, 5, False)) | |
| self.assertIsNone(win.mouse_trafo(7, 19, False)) | |
| def test_putwin(self): | |
| win = curses.newwin(5, 12, 1, 2) | |
| win.addstr(2, 1, 'Lorem ipsum') | |
| with tempfile.TemporaryFile() as f: | |
| win.putwin(f) | |
| del win | |
| f.seek(0) | |
| win = curses.getwin(f) | |
| self.assertEqual(win.getbegyx(), (1, 2)) | |
| self.assertEqual(win.getmaxyx(), (5, 12)) | |
| self.assertEqual(win.instr(2, 0), b' Lorem ipsum') | |
| @requires_curses_func('scr_dump') | |
| def test_scr_dump(self): | |
| # Test scr_dump(), scr_restore(), scr_init() and scr_set(). | |
| # scr_dump() writes the virtual screen to a named file; the other three | |
| # load it back. The dump is opaque internal curses state -- on some | |
| # platforms (such as macOS) it embeds raw pointers that change whenever | |
| # the screen is reallocated -- so the round-trip is exercised | |
| # functionally rather than by comparing dump bytes. | |
| stdscr = self.stdscr | |
| stdscr.erase() | |
| stdscr.addstr(0, 0, 'screen dump test') | |
| stdscr.refresh() | |
| with tempfile.TemporaryDirectory() as d: | |
| dump = os.path.join(d, 'dump') | |
| self.assertIsNone(curses.scr_dump(dump)) | |
| with open(dump, 'rb') as f: | |
| self.assertTrue(f.read()) | |
| # scr_restore() reloads the saved virtual screen, even after the | |
| # screen has changed. | |
| stdscr.erase() | |
| stdscr.addstr(0, 0, 'something else') | |
| stdscr.refresh() | |
| self.assertIsNone(curses.scr_restore(dump)) | |
| # scr_init() and scr_set() also accept a dump file and return None. | |
| # scr_set() is not available on every curses (e.g. old SVr4). | |
| self.assertIsNone(curses.scr_init(dump)) | |
| if hasattr(curses, 'scr_set'): | |
| self.assertIsNone(curses.scr_set(dump)) | |
| # A bytes (path-like) filename is accepted too. | |
| curses.scr_dump(os.fsencode(dump)) | |
| # Restoring from a missing file is an error. | |
| self.assertRaises(curses.error, | |
| curses.scr_restore, os.path.join(d, 'nope')) | |
| def test_borders_and_lines(self): | |
| win = curses.newwin(5, 10, 5, 2) | |
| win.border('|', '!', '-', '_', | |
| '+', '\\', '#', '/') | |
| self.assertEqual(win.instr(0, 0), b'+--------\\') | |
| self.assertEqual(win.instr(1, 0), b'| !') | |
| self.assertEqual(win.instr(4, 0), b'#________/') | |
| win.border(b'|', b'!', b'-', b'_', | |
| b'+', b'\\', b'#', b'/') | |
| win.border(65, 66, 67, 68, | |
| 69, 70, 71, 72) | |
| self.assertRaises(TypeError, win.border, | |
| 65, 66, 67, 68, 69, [], 71, 72) | |
| self.assertRaises(TypeError, win.border, | |
| 65, 66, 67, 68, 69, 70, 71, 72, 73) | |
| self.assertRaises(TypeError, win.border, | |
| 65, 66, 67, 68, 69, 70, 71, 72, 73) | |
| win.border(65, 66, 67, 68, 69, 70, 71) | |
| win.border(65, 66, 67, 68, 69, 70) | |
| win.border(65, 66, 67, 68, 69) | |
| win.border(65, 66, 67, 68) | |
| win.border(65, 66, 67) | |
| win.border(65, 66) | |
| win.border(65) | |
| win.border() | |
| # With no arguments, border() fills the edges with ACS line and corner | |
| # characters. | |
| chartext = curses.A_CHARTEXT | |
| maxy, maxx = win.getmaxyx() | |
| self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) | |
| self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) | |
| self.assertEqual(win.inch(maxy-1, 0) & chartext, curses.ACS_LLCORNER & chartext) | |
| self.assertEqual(win.inch(maxy-1, maxx-1) & chartext, curses.ACS_LRCORNER & chartext) | |
| self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) | |
| self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) | |
| win.box(':', '~') | |
| self.assertEqual(win.instr(0, 1, 8), b'~~~~~~~~') | |
| self.assertEqual(win.instr(1, 0), b': :') | |
| self.assertEqual(win.instr(4, 1, 8), b'~~~~~~~~') | |
| win.box(b':', b'~') | |
| win.box(65, 67) | |
| self.assertRaises(TypeError, win.box, 65, 66, 67) | |
| self.assertRaises(TypeError, win.box, 65) | |
| win.box() | |
| # With no arguments, box() likewise draws ACS corners and lines. | |
| self.assertEqual(win.inch(0, 0) & chartext, curses.ACS_ULCORNER & chartext) | |
| self.assertEqual(win.inch(0, maxx-1) & chartext, curses.ACS_URCORNER & chartext) | |
| self.assertEqual(win.inch(0, 1) & chartext, curses.ACS_HLINE & chartext) | |
| self.assertEqual(win.inch(1, 0) & chartext, curses.ACS_VLINE & chartext) | |
| win.move(1, 2) | |
| win.hline('-', 5) | |
| self.assertEqual(win.instr(1, 1, 7), b' ----- ') | |
| win.hline(b'-', 5) | |
| win.hline(45, 5) | |
| win.hline('-', 5, curses.A_BOLD) | |
| win.hline(1, 1, '-', 5) | |
| win.hline(1, 1, '-', 5, curses.A_BOLD) | |
| win.move(1, 2) | |
| win.vline('a', 3) | |
| win.vline(b'a', 3) | |
| win.vline(97, 3) | |
| win.vline('a', 3, curses.A_STANDOUT) | |
| win.vline(1, 1, 'a', 3) | |
| win.vline(1, 1, ';', 2, curses.A_STANDOUT) | |
| self.assertEqual(win.inch(1, 1), b';'[0] | curses.A_STANDOUT) | |
| self.assertEqual(win.inch(2, 1), b';'[0] | curses.A_STANDOUT) | |
| self.assertEqual(win.inch(3, 1), b'a'[0]) | |
| # A border or line character that fits a single cell byte reads back | |
| # via instr() as that byte and via inch() as the cell value. | |
| for ch in ('é', '¤', '€', 'є'): | |
| try: | |
| b = ch.encode(win.encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(b) != 1: | |
| continue | |
| v = self._char_code(ch) | |
| with self.subTest(ch=ch): | |
| win.erase() | |
| win.hline(2, 0, ch, 5) | |
| self.assertEqual(win.instr(2, 0, 5), b * 5) | |
| self.assertEqual(win.inch(2, 0) & curses.A_CHARTEXT, v) | |
| win.vline(0, 0, ch, 3) | |
| self.assertEqual(win.instr(0, 0, 1), b) | |
| self.assertEqual(win.instr(1, 0, 1), b) | |
| win.border(ch, ch, ch, ch, ch, ch, ch, ch) | |
| self.assertEqual(win.instr(0, 0), b * maxx) | |
| if ord(ch) < 0x100: | |
| # The same byte given as an int. A wide build stores it | |
| # through the locale, so only a Latin-1 byte round-trips. | |
| win.erase() | |
| win.hline(2, 0, v, 5) | |
| self.assertEqual(win.instr(2, 0, 5), b * 5) | |
| win.vline(0, 0, v, 3) | |
| self.assertEqual(win.instr(1, 0, 1), b) | |
| win.border(v, v, v, v, v, v, v, v) | |
| self.assertEqual(win.instr(0, 0), b * maxx) | |
| win.box(v, v) | |
| self.assertEqual(win.instr(0, 1, 1), b) | |
| def test_unctrl(self): | |
| self.assertEqual(curses.unctrl(b'A'), b'A') | |
| self.assertEqual(curses.unctrl('A'), b'A') | |
| self.assertEqual(curses.unctrl(65), b'A') | |
| self.assertEqual(curses.unctrl(b'\n'), b'^J') | |
| self.assertEqual(curses.unctrl('\n'), b'^J') | |
| self.assertEqual(curses.unctrl(10), b'^J') | |
| # A printable non-ASCII byte of an 8-bit locale is returned unchanged. | |
| # See _encodable for the character set. | |
| encoding = self.stdscr.encoding | |
| for ch in ('é', '¤', '€', 'є'): | |
| try: | |
| b = ch.encode(encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(b) != 1: | |
| continue | |
| with self.subTest(ch=ch): | |
| self.assertEqual(curses.unctrl(ch), b) | |
| self.assertEqual(curses.unctrl(b[0]), b) # the byte as an int | |
| self.assertRaises(TypeError, curses.unctrl, b'') | |
| self.assertRaises(TypeError, curses.unctrl, b'AB') | |
| self.assertRaises(TypeError, curses.unctrl, '') | |
| self.assertRaises(TypeError, curses.unctrl, 'AB') | |
| def test_wunctrl(self): | |
| # The wide-character variant of unctrl() returns a str. | |
| self.assertEqual(curses.wunctrl(b'A'), 'A') | |
| self.assertEqual(curses.wunctrl('A'), 'A') | |
| self.assertEqual(curses.wunctrl(65), 'A') | |
| self.assertEqual(curses.wunctrl('\n'), '^J') | |
| self.assertEqual(curses.wunctrl(10), '^J') | |
| # See _encodable for the character set (all printable here). | |
| for c in ('A', 'é', '¤', '€', 'є'): | |
| 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, '') | |
| 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): | |
| if not self.isatty: | |
| self.skipTest('requires terminal') | |
| self.assertIs(curses.isendwin(), False) | |
| curses.endwin() | |
| self.assertIs(curses.isendwin(), True) | |
| curses.doupdate() | |
| self.assertIs(curses.isendwin(), False) | |
| def test_terminfo(self): | |
| self.assertIsInstance(curses.tigetflag('hc'), int) | |
| self.assertEqual(curses.tigetflag('cols'), -1) | |
| self.assertEqual(curses.tigetflag('cr'), -1) | |
| self.assertIsInstance(curses.tigetnum('cols'), int) | |
| self.assertEqual(curses.tigetnum('hc'), -2) | |
| self.assertEqual(curses.tigetnum('cr'), -2) | |
| self.assertIsInstance(curses.tigetstr('cr'), (bytes, type(None))) | |
| self.assertIsNone(curses.tigetstr('hc')) | |
| self.assertIsNone(curses.tigetstr('cols')) | |
| cud = curses.tigetstr('cud') | |
| if cud is not None: | |
| # See issue10570. | |
| self.assertIsInstance(cud, bytes) | |
| curses.tparm(cud, 2) | |
| cud_2 = curses.tparm(cud, 2) | |
| self.assertIsInstance(cud_2, bytes) | |
| curses.putp(cud_2) | |
| curses.putp(b'abc\n') | |
| def test_misc_module_funcs(self): | |
| curses.delay_output(1) | |
| curses.flushinp() | |
| curses.doupdate() | |
| self.assertIs(curses.isendwin(), False) | |
| curses.napms(100) | |
| curses.newpad(50, 50) | |
| def test_env_queries(self): | |
| self.assertIsInstance(curses.termname(), bytes) | |
| self.assertIsInstance(curses.longname(), bytes) | |
| self.assertIsInstance(curses.baudrate(), int) | |
| self.assertIsInstance(curses.has_ic(), bool) | |
| self.assertIsInstance(curses.has_il(), bool) | |
| self.assertIsInstance(curses.termattrs(), int) | |
| if hasattr(curses, 'term_attrs'): | |
| self.assertIsInstance(curses.term_attrs(), int) | |
| c = curses.killchar() | |
| self.assertIsInstance(c, bytes) | |
| self.assertEqual(len(c), 1) | |
| c = curses.erasechar() | |
| self.assertIsInstance(c, bytes) | |
| self.assertEqual(len(c), 1) | |
| # The erase and kill characters are a property of the controlling | |
| # terminal: the wide variants report ERR (raising curses.error) without | |
| # one, while the narrow variants above return an unspecified byte. | |
| try: | |
| tty_fd = os.open(os.ctermid(), os.O_RDONLY) | |
| except OSError: | |
| tty_fd = None | |
| if tty_fd is not None: | |
| os.close(tty_fd) | |
| 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): | |
| # Bind a custom escape sequence to a free key code and read it back. | |
| seq = '\x1bspam' | |
| keycode = 0o600 | |
| curses.define_key(seq, keycode) | |
| self.assertEqual(curses.key_defined(seq), keycode) | |
| # keyok enables or disables interpretation of a single key code. | |
| # Use the key code just defined, which is guaranteed to be known. | |
| self.assertIsNone(curses.keyok(keycode, False)) | |
| self.assertIsNone(curses.keyok(keycode, True)) | |
| # Passing None removes the binding for the key code. | |
| curses.define_key(None, keycode) | |
| self.assertEqual(curses.key_defined(seq), 0) | |
| def test_output_options(self): | |
| stdscr = self.stdscr | |
| stdscr.clearok(True) | |
| stdscr.clearok(False) | |
| stdscr.idcok(True) | |
| stdscr.idcok(False) | |
| stdscr.idlok(False) | |
| stdscr.idlok(True) | |
| if hasattr(stdscr, 'immedok'): | |
| stdscr.immedok(True) | |
| stdscr.immedok(False) | |
| stdscr.leaveok(True) | |
| stdscr.leaveok(False) | |
| stdscr.scrollok(True) | |
| stdscr.scrollok(False) | |
| stdscr.setscrreg(5, 10) | |
| curses.nonl() | |
| curses.nl(True) | |
| curses.nl(False) | |
| curses.nl() | |
| def test_input_options(self): | |
| stdscr = self.stdscr | |
| if self.isatty: | |
| curses.nocbreak() | |
| curses.cbreak() | |
| curses.cbreak(False) | |
| curses.cbreak(True) | |
| curses.intrflush(True) | |
| curses.intrflush(False) | |
| curses.raw() | |
| curses.raw(False) | |
| curses.raw(True) | |
| curses.noraw() | |
| curses.noecho() | |
| curses.echo() | |
| curses.echo(False) | |
| curses.echo(True) | |
| curses.halfdelay(255) | |
| curses.halfdelay(1) | |
| stdscr.keypad(True) | |
| stdscr.keypad(False) | |
| curses.meta(True) | |
| curses.meta(False) | |
| stdscr.nodelay(True) | |
| stdscr.nodelay(False) | |
| curses.noqiflush() | |
| curses.qiflush(True) | |
| curses.qiflush(False) | |
| curses.qiflush() | |
| stdscr.notimeout(True) | |
| stdscr.notimeout(False) | |
| stdscr.timeout(-1) | |
| stdscr.timeout(0) | |
| stdscr.timeout(5) | |
| @requires_curses_window_meth('is_scrollok') | |
| def test_state_getters(self): | |
| stdscr = self.stdscr | |
| # Each is_*() getter returns the value set by the matching setter. | |
| for setter, getter in [ | |
| ('clearok', 'is_cleared'), | |
| ('keypad', 'is_keypad'), | |
| ('leaveok', 'is_leaveok'), | |
| ('nodelay', 'is_nodelay'), | |
| ('notimeout', 'is_notimeout'), | |
| ('scrollok', 'is_scrollok'), | |
| ]: | |
| # is_keypad()/is_leaveok() are not available in every curses build. | |
| if not hasattr(stdscr, getter): | |
| continue | |
| getattr(stdscr, setter)(True) | |
| self.assertIs(getattr(stdscr, getter)(), True) | |
| getattr(stdscr, setter)(False) | |
| self.assertIs(getattr(stdscr, getter)(), False) | |
| # idcok()/idlok() only take effect if the terminal can insert/delete | |
| # characters/lines, so the getter reflects that capability. | |
| stdscr.idcok(True) | |
| self.assertIs(stdscr.is_idcok(), curses.has_ic()) | |
| stdscr.idcok(False) | |
| self.assertIs(stdscr.is_idcok(), False) | |
| stdscr.idlok(True) | |
| self.assertIs(stdscr.is_idlok(), | |
| curses.has_il() or curses.tigetstr('csr') is not None) | |
| stdscr.idlok(False) | |
| self.assertIs(stdscr.is_idlok(), False) | |
| if hasattr(stdscr, 'immedok'): | |
| stdscr.immedok(True) | |
| self.assertIs(stdscr.is_immedok(), True) | |
| stdscr.immedok(False) | |
| if hasattr(stdscr, 'syncok'): | |
| stdscr.syncok(True) | |
| self.assertIs(stdscr.is_syncok(), True) | |
| stdscr.syncok(False) | |
| # getdelay() reflects timeout()/nodelay(). | |
| stdscr.timeout(100) | |
| self.assertEqual(stdscr.getdelay(), 100) | |
| stdscr.nodelay(True) | |
| self.assertEqual(stdscr.getdelay(), 0) | |
| stdscr.timeout(-1) | |
| self.assertEqual(stdscr.getdelay(), -1) | |
| # getscrreg() reflects setscrreg(). | |
| stdscr.setscrreg(5, 10) | |
| self.assertEqual(stdscr.getscrreg(), (5, 10)) | |
| # is_pad()/is_subwin(). | |
| self.assertIs(stdscr.is_pad(), False) | |
| self.assertIs(stdscr.is_subwin(), False) | |
| sub = stdscr.subwin(3, 3, 0, 0) | |
| self.assertIs(sub.is_subwin(), True) | |
| pad = curses.newpad(5, 5) | |
| self.assertIs(pad.is_pad(), True) | |
| @requires_curses_func('is_cbreak') | |
| def test_global_state_getters(self): | |
| if self.isatty: | |
| curses.cbreak() | |
| self.assertIs(curses.is_cbreak(), True) | |
| curses.nocbreak() | |
| self.assertIs(curses.is_cbreak(), False) | |
| curses.raw() | |
| self.assertIs(curses.is_raw(), True) | |
| curses.noraw() | |
| self.assertIs(curses.is_raw(), False) | |
| curses.echo() | |
| self.assertIs(curses.is_echo(), True) | |
| curses.noecho() | |
| self.assertIs(curses.is_echo(), False) | |
| curses.nl() | |
| self.assertIs(curses.is_nl(), True) | |
| curses.nonl() | |
| self.assertIs(curses.is_nl(), False) | |
| @requires_curses_func('typeahead') | |
| def test_typeahead(self): | |
| curses.typeahead(sys.__stdin__.fileno()) | |
| curses.typeahead(-1) | |
| def test_prog_mode(self): | |
| if not self.isatty: | |
| self.skipTest('requires terminal') | |
| curses.def_prog_mode() | |
| curses.reset_prog_mode() | |
| # def_shell_mode()/reset_shell_mode() are intentionally not exercised | |
| # here: they capture and restore curses' "shell mode" terminal state, | |
| # which is only meaningful before initscr(). Calling them mid-suite | |
| # corrupts the modes that endwin() restores and breaks later tests. | |
| def test_beep(self): | |
| if (curses.tigetstr("bel") is not None | |
| or curses.tigetstr("flash") is not None): | |
| curses.beep() | |
| else: | |
| try: | |
| curses.beep() | |
| except curses.error: | |
| self.skipTest('beep() failed') | |
| def test_flash(self): | |
| if (curses.tigetstr("bel") is not None | |
| or curses.tigetstr("flash") is not None): | |
| curses.flash() | |
| else: | |
| try: | |
| curses.flash() | |
| except curses.error: | |
| self.skipTest('flash() failed') | |
| def test_curs_set(self): | |
| for vis, cap in [(0, 'civis'), (2, 'cvvis'), (1, 'cnorm')]: | |
| if curses.tigetstr(cap) is not None: | |
| curses.curs_set(vis) | |
| else: | |
| try: | |
| curses.curs_set(vis) | |
| except curses.error: | |
| pass | |
| @requires_curses_func('get_escdelay') | |
| def test_escdelay(self): | |
| escdelay = curses.get_escdelay() | |
| self.assertIsInstance(escdelay, int) | |
| curses.set_escdelay(25) | |
| self.assertEqual(curses.get_escdelay(), 25) | |
| curses.set_escdelay(escdelay) | |
| @requires_curses_func('get_tabsize') | |
| def test_tabsize(self): | |
| tabsize = curses.get_tabsize() | |
| self.assertIsInstance(tabsize, int) | |
| # set_tabsize() is not available on every curses (e.g. old SVr4). | |
| if hasattr(curses, 'set_tabsize'): | |
| curses.set_tabsize(4) | |
| self.assertEqual(curses.get_tabsize(), 4) | |
| curses.set_tabsize(tabsize) | |
| @requires_curses_func('getsyx') | |
| def test_getsyx(self): | |
| y, x = curses.getsyx() | |
| self.assertIsInstance(y, int) | |
| self.assertIsInstance(x, int) | |
| curses.setsyx(4, 5) | |
| self.assertEqual(curses.getsyx(), (4, 5)) | |
| def bad_colors(self): | |
| return (-1, curses.COLORS, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) | |
| def bad_colors2(self): | |
| return (curses.COLORS, 2**31, 2**63, 2**64) | |
| def bad_pairs(self): | |
| return (-1, -2**31 - 1, 2**31, -2**63 - 1, 2**63, 2**64) | |
| def test_has_colors(self): | |
| self.assertIsInstance(curses.has_colors(), bool) | |
| self.assertIsInstance(curses.can_change_color(), bool) | |
| @requires_curses_func('has_mouse') | |
| def test_has_mouse(self): | |
| # Whether a mouse is available depends on the terminal. | |
| self.assertIsInstance(curses.has_mouse(), bool) | |
| def test_start_color(self): | |
| if not curses.has_colors(): | |
| self.skipTest('requires colors support') | |
| curses.start_color() | |
| if verbose: | |
| print(f'COLORS = {curses.COLORS}', file=sys.stderr) | |
| print(f'COLOR_PAIRS = {curses.COLOR_PAIRS}', file=sys.stderr) | |
| @requires_colors | |
| def test_color_content(self): | |
| self.assertEqual(curses.color_content(curses.COLOR_BLACK), (0, 0, 0)) | |
| curses.color_content(0) | |
| maxcolor = curses.COLORS - 1 | |
| curses.color_content(maxcolor) | |
| for color in self.bad_colors(): | |
| self.assertRaises(ValueError, curses.color_content, color) | |
| @requires_colors | |
| def test_init_color(self): | |
| if not curses.can_change_color(): | |
| self.skipTest('cannot change color') | |
| old = curses.color_content(0) | |
| try: | |
| curses.init_color(0, *old) | |
| except curses.error: | |
| self.skipTest('cannot change color (init_color() failed)') | |
| self.addCleanup(curses.init_color, 0, *old) | |
| curses.init_color(0, 0, 0, 0) | |
| self.assertEqual(curses.color_content(0), (0, 0, 0)) | |
| curses.init_color(0, 1000, 1000, 1000) | |
| self.assertEqual(curses.color_content(0), (1000, 1000, 1000)) | |
| maxcolor = curses.COLORS - 1 | |
| old = curses.color_content(maxcolor) | |
| curses.init_color(maxcolor, *old) | |
| self.addCleanup(curses.init_color, maxcolor, *old) | |
| curses.init_color(maxcolor, 0, 500, 1000) | |
| self.assertEqual(curses.color_content(maxcolor), (0, 500, 1000)) | |
| for color in self.bad_colors(): | |
| self.assertRaises(ValueError, curses.init_color, color, 0, 0, 0) | |
| for comp in (-1, 1001): | |
| self.assertRaises(ValueError, curses.init_color, 0, comp, 0, 0) | |
| self.assertRaises(ValueError, curses.init_color, 0, 0, comp, 0) | |
| self.assertRaises(ValueError, curses.init_color, 0, 0, 0, comp) | |
| def get_pair_limit(self): | |
| pair_limit = curses.COLOR_PAIRS | |
| if hasattr(curses, 'ncurses_version'): | |
| if curses.has_extended_color_support(): | |
| pair_limit += 2*curses.COLORS + 1 | |
| if (not curses.has_extended_color_support() | |
| or (6, 1) <= curses.ncurses_version < (6, 2)): | |
| pair_limit = min(pair_limit, SHORT_MAX) | |
| # If use_default_colors() is called, the upper limit of the extended | |
| # range may be restricted, so we need to check if the limit is still | |
| # correct | |
| try: | |
| curses.init_pair(pair_limit - 1, 0, 0) | |
| except ValueError: | |
| pair_limit = curses.COLOR_PAIRS | |
| return pair_limit | |
| @requires_colors | |
| def test_pair_content(self): | |
| curses.pair_content(0) | |
| maxpair = self.get_pair_limit() - 1 | |
| if maxpair > 0: | |
| curses.pair_content(maxpair) | |
| for pair in self.bad_pairs(): | |
| self.assertRaises(ValueError, curses.pair_content, pair) | |
| @requires_colors | |
| def test_init_pair(self): | |
| old = curses.pair_content(1) | |
| curses.init_pair(1, *old) | |
| self.addCleanup(curses.init_pair, 1, *old) | |
| curses.init_pair(1, 0, 0) | |
| self.assertEqual(curses.pair_content(1), (0, 0)) | |
| maxcolor = curses.COLORS - 1 | |
| curses.init_pair(1, maxcolor, 0) | |
| self.assertEqual(curses.pair_content(1), (maxcolor, 0)) | |
| curses.init_pair(1, 0, maxcolor) | |
| self.assertEqual(curses.pair_content(1), (0, maxcolor)) | |
| maxpair = self.get_pair_limit() - 1 | |
| if maxpair > 1: | |
| curses.init_pair(maxpair, 0, 0) | |
| self.assertEqual(curses.pair_content(maxpair), (0, 0)) | |
| for pair in self.bad_pairs(): | |
| self.assertRaises(ValueError, curses.init_pair, pair, 0, 0) | |
| for color in self.bad_colors2(): | |
| self.assertRaises(ValueError, curses.init_pair, 1, color, 0) | |
| self.assertRaises(ValueError, curses.init_pair, 1, 0, color) | |
| @requires_curses_func('alloc_pair') | |
| @requires_colors | |
| def test_dynamic_color_pairs(self): | |
| # alloc_pair()/find_pair()/free_pair() (extended-color extension). | |
| fg = bg = curses.COLORS - 1 | |
| pair = curses.alloc_pair(fg, bg) | |
| self.assertGreater(pair, 0) | |
| self.assertEqual(curses.pair_content(pair), (fg, bg)) | |
| if getattr(curses, 'ncurses_version', (6, 3)) >= (6, 3): | |
| # The same combination of colors reuses the same pair. | |
| self.assertEqual(curses.alloc_pair(fg, bg), pair) | |
| self.assertEqual(curses.find_pair(fg, bg), pair) | |
| # Once freed, the pair is no longer found. | |
| self.assertIsNone(curses.free_pair(pair)) | |
| self.assertEqual(curses.find_pair(fg, bg), -1) | |
| else: | |
| self.assertIsNone(curses.free_pair(pair)) | |
| # Error paths. | |
| for color in self.bad_colors2(): | |
| self.assertRaises(ValueError, curses.alloc_pair, color, 0) | |
| self.assertRaises(ValueError, curses.alloc_pair, 0, color) | |
| self.assertRaises(ValueError, curses.find_pair, color, 0) | |
| self.assertRaises(ValueError, curses.find_pair, 0, color) | |
| for pair in self.bad_pairs(): | |
| self.assertRaises(ValueError, curses.free_pair, pair) | |
| # Color pair 0 is reserved and cannot be freed. | |
| self.assertRaises(curses.error, curses.free_pair, 0) | |
| # Invalid number or type of arguments. | |
| self.assertRaises(TypeError, curses.alloc_pair) | |
| self.assertRaises(TypeError, curses.alloc_pair, 0) | |
| self.assertRaises(TypeError, curses.alloc_pair, 0, 0, 0) | |
| self.assertRaises(TypeError, curses.alloc_pair, 'red', 0) | |
| self.assertRaises(TypeError, curses.alloc_pair, 0, 'red') | |
| self.assertRaises(TypeError, curses.alloc_pair, fg=0, bg=0) | |
| self.assertRaises(TypeError, curses.find_pair) | |
| self.assertRaises(TypeError, curses.find_pair, 0) | |
| self.assertRaises(TypeError, curses.find_pair, 0, 0, 0) | |
| self.assertRaises(TypeError, curses.find_pair, 'red', 0) | |
| self.assertRaises(TypeError, curses.find_pair, 0, 'red') | |
| self.assertRaises(TypeError, curses.free_pair) | |
| self.assertRaises(TypeError, curses.free_pair, 1, 2) | |
| self.assertRaises(TypeError, curses.free_pair, 'red') | |
| @requires_curses_func('reset_color_pairs') | |
| @requires_colors | |
| def test_reset_color_pairs(self): | |
| self.assertIsNone(curses.reset_color_pairs()) | |
| self.assertRaises(TypeError, curses.reset_color_pairs, 0) | |
| @requires_colors | |
| def test_color_attrs(self): | |
| for pair in 0, 1, 255: | |
| attr = curses.color_pair(pair) | |
| self.assertEqual(curses.pair_number(attr), pair, attr) | |
| self.assertEqual(curses.pair_number(attr | curses.A_BOLD), pair) | |
| self.assertEqual(curses.color_pair(0), 0) | |
| self.assertEqual(curses.pair_number(0), 0) | |
| # A pair too large to fit is rejected, not silently masked (gh-119138). | |
| max_pair = curses.pair_number(curses.A_COLOR) | |
| self.assertEqual(curses.pair_number(curses.color_pair(max_pair)), max_pair) | |
| self.assertRaises(OverflowError, curses.color_pair, max_pair + 1) | |
| self.assertRaises(OverflowError, curses.color_pair, -1) | |
| @requires_curses_func('use_default_colors') | |
| @requires_colors | |
| def test_use_default_colors(self): | |
| try: | |
| curses.use_default_colors() | |
| except curses.error: | |
| self.skipTest('cannot change color (use_default_colors() failed)') | |
| self.assertEqual(curses.pair_content(0), (-1, -1)) | |
| @requires_curses_window_meth('use') | |
| def test_use_window(self): | |
| win = self.stdscr | |
| self.assertEqual(win.use(lambda w, a, b: (w is win, a, b), 5, b=6), | |
| (True, 5, 6)) | |
| with self.assertRaises(ZeroDivisionError): | |
| win.use(lambda w: 1 / 0) | |
| @unittest.skipUnless(hasattr(curses.screen, 'use'), | |
| 'requires screen.use()') | |
| @unittest.skipUnless(USE_NEWTERM, 'no screen object without newterm()') | |
| def test_use_screen(self): | |
| screen = self.screen | |
| self.assertEqual( | |
| screen.use(lambda sc, flag: (sc is screen, flag), flag=True), | |
| (True, True)) | |
| @requires_curses_func('assume_default_colors') | |
| @requires_colors | |
| def test_assume_default_colors(self): | |
| try: | |
| curses.assume_default_colors(-1, -1) | |
| except curses.error: | |
| self.skipTest('cannot change color (assume_default_colors() failed)') | |
| self.assertEqual(curses.pair_content(0), (-1, -1)) | |
| curses.assume_default_colors(curses.COLOR_YELLOW, curses.COLOR_BLUE) | |
| self.assertEqual(curses.pair_content(0), (curses.COLOR_YELLOW, curses.COLOR_BLUE)) | |
| curses.assume_default_colors(curses.COLOR_RED, -1) | |
| self.assertEqual(curses.pair_content(0), (curses.COLOR_RED, -1)) | |
| curses.assume_default_colors(-1, curses.COLOR_GREEN) | |
| self.assertEqual(curses.pair_content(0), (-1, curses.COLOR_GREEN)) | |
| curses.assume_default_colors(-1, -1) | |
| def test_keyname(self): | |
| # TODO: key_name() | |
| self.assertEqual(curses.keyname(65), b'A') | |
| self.assertEqual(curses.keyname(13), b'^M') | |
| self.assertEqual(curses.keyname(127), b'^?') | |
| self.assertEqual(curses.keyname(0), b'^@') | |
| self.assertRaises(ValueError, curses.keyname, -1) | |
| self.assertIsInstance(curses.keyname(256), bytes) | |
| @requires_curses_func('has_key') | |
| def test_has_key(self): | |
| self.assertIsInstance(curses.has_key(13), bool) | |
| self.assertIsInstance(curses.has_key(curses.KEY_LEFT), bool) | |
| @requires_curses_func('getmouse') | |
| def test_getmouse(self): | |
| (availmask, oldmask) = curses.mousemask(curses.BUTTON1_PRESSED) | |
| if availmask == 0: | |
| self.skipTest('mouse stuff not available') | |
| curses.mouseinterval(10) | |
| # just verify these don't cause errors | |
| curses.ungetmouse(0, 0, 0, 0, curses.BUTTON1_PRESSED) | |
| m = curses.getmouse() | |
| @requires_curses_func('panel') | |
| def test_userptr_without_set(self): | |
| w = curses.newwin(10, 10) | |
| p = curses.panel.new_panel(w) | |
| # try to access userptr() before calling set_userptr() -- segfaults | |
| with self.assertRaises(curses.panel.error, | |
| msg='userptr should fail since not set'): | |
| p.userptr() | |
| @requires_curses_func('panel') | |
| def test_userptr_memory_leak(self): | |
| w = curses.newwin(10, 10) | |
| p = curses.panel.new_panel(w) | |
| obj = object() | |
| nrefs = sys.getrefcount(obj) | |
| for i in range(100): | |
| p.set_userptr(obj) | |
| p.set_userptr(None) | |
| self.assertEqual(sys.getrefcount(obj), nrefs, | |
| "set_userptr leaked references") | |
| @requires_curses_func('panel') | |
| def test_userptr_segfault(self): | |
| w = curses.newwin(10, 10) | |
| panel = curses.panel.new_panel(w) | |
| # set_userptr(A()) makes a panel<->userptr reference cycle (A.__del__ | |
| # closes over panel); clean it up so the panel and its window do not | |
| # linger until a later test collects them. | |
| self.addCleanup(self._delete_panels, panel) | |
| class A: | |
| def __del__(self): | |
| panel.set_userptr(None) | |
| panel.set_userptr(A()) | |
| panel.set_userptr(None) | |
| @requires_curses_func('panel') | |
| def test_userptr_dealloc_segfault(self): | |
| w = curses.newwin(10, 10) | |
| panel = curses.panel.new_panel(w) | |
| seen = [] | |
| class A: | |
| def __del__(self): | |
| # The panel is being deallocated, so it must already be off | |
| # the stack: handing it back here would resurrect an object | |
| # whose refcount is zero -- segfaults. | |
| seen.append(curses.panel.top_panel() is None) | |
| panel.set_userptr(A()) | |
| del panel | |
| gc_collect() | |
| self.assertEqual(seen, [True]) | |
| @cpython_only | |
| @requires_curses_func('panel') | |
| def test_disallow_instantiation(self): | |
| # Ensure that the type disallows instantiation (bpo-43916) | |
| w = curses.newwin(10, 10) | |
| panel = curses.panel.new_panel(w) | |
| check_disallow_instantiation(self, type(panel)) | |
| @requires_curses_func('panel') | |
| def test_panel_stack(self): | |
| panel = curses.panel | |
| # new_panel() puts the panel on top of the stack, so the three | |
| # panels end up ordered bottom -> top as p1, p2, p3. | |
| p1 = panel.new_panel(curses.newwin(3, 6, 0, 0)) | |
| p2 = panel.new_panel(curses.newwin(3, 6, 1, 1)) | |
| p3 = panel.new_panel(curses.newwin(3, 6, 2, 2)) | |
| self.addCleanup(self._delete_panels, p1, p2, p3) | |
| # The most recently created panel is on top. | |
| self.assertIs(panel.top_panel(), p3) | |
| # window() returns the wrapped window. | |
| self.assertEqual(p2.window().getbegyx(), (1, 1)) | |
| # above()/below() walk the stack one step at a time. | |
| self.assertIs(p1.above(), p2) | |
| self.assertIs(p2.above(), p3) | |
| self.assertIsNone(p3.above()) # nothing above the top panel | |
| self.assertIs(p3.below(), p2) | |
| self.assertIs(p2.below(), p1) | |
| # top() raises a panel to the top, bottom() lowers it to the bottom. | |
| p1.top() | |
| self.assertIs(panel.top_panel(), p1) | |
| self.assertIsNone(p1.above()) | |
| p1.bottom() | |
| self.assertIs(panel.bottom_panel(), p1) | |
| self.assertIsNone(p1.below()) | |
| # update_panels() refreshes the virtual screen from the stack. | |
| panel.update_panels() | |
| @requires_curses_func('panel') | |
| def test_panel_hide_show(self): | |
| p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) | |
| self.addCleanup(self._delete_panels, p) | |
| self.assertIs(p.hidden(), False) | |
| p.hide() | |
| self.assertIs(p.hidden(), True) | |
| p.show() | |
| self.assertIs(p.hidden(), False) | |
| @requires_curses_func('panel') | |
| def test_panel_move(self): | |
| win = curses.newwin(3, 6, 1, 2) | |
| p = curses.panel.new_panel(win) | |
| self.addCleanup(self._delete_panels, p) | |
| self.assertEqual(win.getbegyx(), (1, 2)) | |
| p.move(4, 5) | |
| self.assertEqual(win.getbegyx(), (4, 5)) | |
| @requires_curses_func('panel') | |
| def test_panel_replace(self): | |
| win1 = curses.newwin(3, 6, 0, 0) | |
| win2 = curses.newwin(4, 8, 1, 1) | |
| p = curses.panel.new_panel(win1) | |
| self.addCleanup(self._delete_panels, p) | |
| self.assertIs(p.window(), win1) | |
| p.replace(win2) | |
| self.assertIs(p.window(), win2) | |
| @requires_curses_func('panel') | |
| def test_panel_userptr(self): | |
| p = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) | |
| self.addCleanup(self._delete_panels, p) | |
| obj = ['userptr'] | |
| p.set_userptr(obj) | |
| self.assertIs(p.userptr(), obj) | |
| def _delete_panels(self, *panels): | |
| # Drop the panels from the global stack so they do not leak into | |
| # later tests that inspect top_panel()/bottom_panel(). | |
| for p in panels: | |
| try: | |
| p.bottom() | |
| except curses.panel.error: | |
| pass | |
| del panels | |
| gc_collect() | |
| def _make_textbox(self, nlines, ncols, *, insert_mode=False, stripspaces=1): | |
| win = curses.newwin(nlines, ncols, 0, 0) | |
| box = curses.textpad.Textbox(win, insert_mode=insert_mode) | |
| box.stripspaces = stripspaces | |
| return box, win | |
| def _type(self, box, text): | |
| for ch in text: | |
| box.do_command(ch if isinstance(ch, int) else ord(ch)) | |
| def test_textbox_gather(self): | |
| # Typed text is read back by gather(). With stripspaces on (the | |
| # default) gather() keeps a single trailing blank on a line and | |
| # drops trailing empty lines. | |
| box, win = self._make_textbox(3, 10) | |
| self._type(box, 'Hello') | |
| self.assertEqual(box.gather(), 'Hello \n') | |
| def test_textbox_gather_multiline(self): | |
| box, win = self._make_textbox(3, 10) | |
| self._type(box, 'ab') | |
| box.do_command(curses.ascii.NL) # ^j -> start of next line | |
| self._type(box, 'cd') | |
| self.assertEqual(box.gather(), 'ab \ncd \n') | |
| def test_textbox_stripspaces(self): | |
| box, win = self._make_textbox(1, 8, stripspaces=1) | |
| self._type(box, 'hi') | |
| self.assertEqual(box.gather(), 'hi ') | |
| box, win = self._make_textbox(1, 8, stripspaces=0) | |
| self._type(box, 'hi') | |
| self.assertEqual(box.gather(), 'hi ') | |
| def test_textbox_insert_mode(self): | |
| # In insert mode a typed character shifts the rest of the line right. | |
| box, win = self._make_textbox(1, 10, insert_mode=True) | |
| self._type(box, 'aXc') | |
| win.move(0, 1) | |
| self._type(box, 'b') | |
| self.assertEqual(box.gather(), 'abXc ') | |
| def test_textbox_fill_last_cell(self): | |
| # The lower-right cell can be written, even though addch() there | |
| # cannot advance the cursor past the end of the window. | |
| box, win = self._make_textbox(1, 4, stripspaces=0) | |
| self._type(box, 'abcd') | |
| self.assertEqual(box.gather(), 'abcd') | |
| def test_textbox_fill_last_cell_multiline(self): | |
| box, win = self._make_textbox(2, 3, stripspaces=0) | |
| self._type(box, 'abc') | |
| box.do_command(curses.ascii.NL) # ^j -> start of next line | |
| self._type(box, 'def') # 'f' lands in the lower-right cell | |
| self.assertEqual(box.gather(), 'abc\ndef\n') | |
| def test_textbox_fill_last_cell_insert_mode(self): | |
| box, win = self._make_textbox(1, 4, insert_mode=True, stripspaces=0) | |
| self._type(box, 'abcd') | |
| self.assertEqual(box.gather(), 'abcd') | |
| def test_textbox_fill_last_cell_scrollok(self): | |
| # Writing the lower-right cell must not scroll the window even if it | |
| # has scrolling enabled. | |
| box, win = self._make_textbox(2, 3, stripspaces=0) | |
| win.scrollok(True) | |
| self._type(box, 'abc') | |
| box.do_command(curses.ascii.NL) | |
| self._type(box, 'def') | |
| self.assertEqual(box.gather(), 'abc\ndef\n') | |
| def test_textbox_8bit(self): | |
| # An 8-bit-locale character is entered as integer bytes -- the way | |
| # do_command() receives getch() input -- and read back; runs on both | |
| # builds. Run the suite under an 8-bit locale | |
| # (ISO-8859-1, ISO-8859-15 or KOI8-U) to reach the non-ASCII cases; each | |
| # string is used only if the encoding maps it to single bytes. 'abc' is | |
| # ASCII, 'café' is common to the Latin encodings, and the rest are | |
| # distinctive (byte 0xA4 is '¤'/'€'/'є' in ISO-8859-1/-15/KOI8-U). | |
| encoding = self.stdscr.encoding | |
| for text in ['abc', 'café', 'naïve ¤¦', 'café €Šž', 'дякую єі']: | |
| try: | |
| data = text.encode(encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(data) != len(text): | |
| continue # a multibyte encoding is not the 8-bit byte path | |
| with self.subTest(text=text): | |
| box, win = self._make_textbox(1, 16) | |
| for byte in data: | |
| box.do_command(byte) | |
| self.assertEqual(box.gather(), text + ' ') | |
| def test_textbox_8bit_insert(self): | |
| # Insert mode shifts the rest of the line right by reading each cell back | |
| # and rewriting it; an 8-bit-locale character entered as bytes must | |
| # survive the shift. See test_textbox_8bit for the character choices. | |
| encoding = self.stdscr.encoding | |
| for ch in ['é', '¤', '€', 'є']: | |
| try: | |
| data = ch.encode(encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(data) != 1: | |
| continue | |
| with self.subTest(ch=ch): | |
| box, win = self._make_textbox(1, 10, insert_mode=True) | |
| for byte in ('a' + ch + 'c').encode(encoding): | |
| box.do_command(byte) | |
| win.move(0, 1) | |
| box.do_command(ord('b')) # insert 'b', shifting ch and 'c' right | |
| self.assertEqual(box.gather(), 'ab' + ch + 'c ') | |
| def test_textbox_8bit_fill_last_cell(self): | |
| # An 8-bit-locale character entered as bytes must survive being written | |
| # to the lower-right cell, which uses insch() rather than addch(). See | |
| # test_textbox_8bit for the character choices. | |
| encoding = self.stdscr.encoding | |
| for ch in ['é', '¤', '€', 'є']: | |
| try: | |
| data = ch.encode(encoding) | |
| except UnicodeEncodeError: | |
| continue | |
| if len(data) != 1: | |
| continue | |
| with self.subTest(ch=ch): | |
| text = 'ab' + ch # the last character fills the corner | |
| box, win = self._make_textbox(1, len(text), stripspaces=0) | |
| for byte in text.encode(encoding): | |
| box.do_command(byte) | |
| self.assertEqual(box.gather(), text) | |
| def test_textbox_unicode(self): | |
| # Like test_textbox_8bit, but characters are entered as strings -- the | |
| # way do_command() receives get_wch() input -- rather than integer | |
| # bytes. Each string is used only if encodable in the current locale; | |
| # a narrow build stores one byte per cell, so multi-byte characters | |
| # additionally need a wide build. | |
| for text in ['abc', 'héšλ', 'café', 'naïve ¤', 'soupçon €Š', 'дякую єі']: | |
| if not self._encodable(text): | |
| continue | |
| if not WIDE_BUILD and len(text.encode(self.stdscr.encoding)) != len(text): | |
| continue | |
| with self.subTest(text=text): | |
| box, win = self._make_textbox(1, 12) | |
| for ch in text: | |
| box.do_command(ch) | |
| self.assertEqual(box.gather(), text + ' ') | |
| def test_textbox_unicode_insert_mode(self): | |
| # Like test_textbox_8bit_insert, but the character is entered as a string | |
| # (get_wch() input). Each string is used only if encodable; multi-byte | |
| # characters additionally need a wide build (one byte per cell otherwise). | |
| for text in ['abcd', 'aβλc', 'aéàc', 'a¤½c', 'a€Šc', 'aдві']: | |
| if not self._encodable(text): | |
| continue | |
| if not WIDE_BUILD and len(text.encode(self.stdscr.encoding)) != len(text): | |
| continue | |
| with self.subTest(text=text): | |
| box, win = self._make_textbox(1, 10, insert_mode=True) | |
| for ch in text[0] + text[2:]: # all but the 2nd character | |
| box.do_command(ch) | |
| win.move(0, 1) | |
| box.do_command(text[1]) # insert it at position 1 | |
| self.assertEqual(box.gather(), text + ' ') | |
| @requires_wide_build | |
| def test_textbox_combining(self): | |
| # A spacing character plus a combining mark is a single cell, which | |
| # needs the wide build (a narrow build stores one byte per cell). | |
| text = 'e\u0301' # 'e' + COMBINING ACUTE ACCENT | |
| if self._encodable(text): | |
| box, win = self._make_textbox(1, 10) | |
| for ch in text: | |
| box.do_command(ch) | |
| self.assertEqual(box.gather(), text + ' ') | |
| @requires_wide_build | |
| def test_textbox_double_width(self): | |
| # A double-width (East Asian) character occupies two cells. gather() | |
| # reads a whole line at a time so that the second cell, which holds | |
| # the same character, is not reported as another one. | |
| text = '你好' | |
| if not self._encodable(text): | |
| self.skipTest('the locale cannot encode %r' % text) | |
| box, win = self._make_textbox(1, 12) | |
| for ch in text: | |
| box.do_command(ch) | |
| self.assertEqual(box.gather(), text + ' ') | |
| box, win = self._make_textbox(1, 12, stripspaces=False) | |
| for ch in text: | |
| box.do_command(ch) | |
| self.assertEqual(box.gather(), text + ' ' * 8) | |
| def test_textbox_edit_wide(self): | |
| # edit() reads characters through get_wch(). Each character is pushed | |
| # with unget_wch(), which on a narrow build requires it to encode to a | |
| # single byte, so a non-ASCII case needs a wide build or an 8-bit locale. | |
| for ch in ['A', 'é', '¤', '€', 'д']: | |
| if not self._encodable(ch): | |
| continue | |
| if not WIDE_BUILD and len(ch.encode(self.stdscr.encoding)) != 1: | |
| continue | |
| with self.subTest(ch=ch): | |
| box, win = self._make_textbox(1, 10) | |
| for c in reversed(['a', ch, chr(curses.ascii.BEL)]): | |
| curses.unget_wch(c) | |
| self.assertEqual(box.edit(), 'a' + ch + ' ') | |
| def test_textbox_movement(self): | |
| box, win = self._make_textbox(3, 10) | |
| self._type(box, 'abc') | |
| box.do_command(curses.ascii.SOH) # ^a -> left edge | |
| self.assertEqual(win.getyx(), (0, 0)) | |
| box.do_command(curses.ascii.ENQ) # ^e -> end of line | |
| self.assertEqual(win.getyx(), (0, 3)) | |
| def test_textbox_kill_to_eol(self): | |
| box, win = self._make_textbox(1, 10) | |
| self._type(box, 'abcdef') | |
| win.move(0, 3) | |
| box.do_command(curses.ascii.VT) # ^k -> clear to end of line | |
| self.assertEqual(box.gather(), 'abc ') | |
| def test_textbox_backspace(self): | |
| box, win = self._make_textbox(1, 10) | |
| self._type(box, 'abc') | |
| box.do_command(curses.ascii.BS) # ^h -> delete backward | |
| self.assertEqual(box.gather(), 'ab ') | |
| def test_textbox_edit(self): | |
| # edit() reads characters until Ctrl-G and returns the contents. | |
| box, win = self._make_textbox(1, 10) | |
| for ch in reversed('Hi' + chr(curses.ascii.BEL)): | |
| curses.ungetch(ch) | |
| self.assertEqual(box.edit(), 'Hi ') | |
| def test_textbox_edit_validate(self): | |
| # The validate hook can rewrite an incoming keystroke. | |
| box, win = self._make_textbox(1, 10) | |
| for ch in reversed('abc' + chr(curses.ascii.BEL)): | |
| curses.ungetch(ch) | |
| box.edit(lambda ch: ord('X') if ch == ord('b') else ch) | |
| self.assertEqual(box.gather(), 'aXc ') | |
| def test_textpad_rectangle(self): | |
| # rectangle() draws a box with ACS line/corner characters. | |
| win = curses.newwin(6, 12, 0, 0) | |
| curses.textpad.rectangle(win, 0, 0, 4, 8) | |
| chartext = curses.A_CHARTEXT | |
| self.assertEqual(win.inch(0, 0) & chartext, | |
| curses.ACS_ULCORNER & chartext) | |
| self.assertEqual(win.inch(0, 8) & chartext, | |
| curses.ACS_URCORNER & chartext) | |
| self.assertEqual(win.inch(4, 0) & chartext, | |
| curses.ACS_LLCORNER & chartext) | |
| self.assertEqual(win.inch(4, 8) & chartext, | |
| curses.ACS_LRCORNER & chartext) | |
| self.assertEqual(win.inch(0, 1) & chartext, | |
| curses.ACS_HLINE & chartext) | |
| self.assertEqual(win.inch(1, 0) & chartext, | |
| curses.ACS_VLINE & chartext) | |
| def test_wrapper(self): | |
| # wrapper() sets up curses, passes the screen to the callable along | |
| # with extra arguments, returns its result and restores the terminal. | |
| if not self.isatty: | |
| self.skipTest('requires terminal') | |
| def body(stdscr, a, b): | |
| self.assertIsInstance(stdscr, type(self.stdscr)) | |
| self.assertIs(curses.isendwin(), False) | |
| return a + b | |
| self.assertEqual(curses.wrapper(body, 2, 3), 5) | |
| self.assertIs(curses.isendwin(), True) | |
| # wrapper() left the screen ended; revive it so the per-test | |
| # endwin() cleanup does not fail with ERR. | |
| curses.doupdate() | |
| @requires_curses_func('is_term_resized') | |
| def test_is_term_resized(self): | |
| lines, cols = curses.LINES, curses.COLS | |
| self.assertIs(curses.is_term_resized(lines, cols), False) | |
| self.assertIs(curses.is_term_resized(lines-1, cols-1), True) | |
| @requires_curses_func('resize_term') | |
| def test_resize_term(self): | |
| curses.update_lines_cols() | |
| lines, cols = curses.LINES, curses.COLS | |
| new_lines = lines - 1 | |
| new_cols = cols + 1 | |
| curses.resize_term(new_lines, new_cols) | |
| self.assertEqual(curses.LINES, new_lines) | |
| self.assertEqual(curses.COLS, new_cols) | |
| curses.resize_term(lines, cols) | |
| self.assertEqual(curses.LINES, lines) | |
| self.assertEqual(curses.COLS, cols) | |
| with self.assertRaises(OverflowError): | |
| curses.resize_term(35000, 1) | |
| with self.assertRaises(OverflowError): | |
| curses.resize_term(1, 35000) | |
| # GH-120378: a failed resize can leave refresh broken; restore the | |
| # original size to recover. Avoid initscr(), which would switch away | |
| # from the shared newterm() screen and corrupt later tests. | |
| curses.resize_term(lines, cols) | |
| self.stdscr.erase() | |
| @requires_curses_func('resizeterm') | |
| def test_resizeterm(self): | |
| curses.update_lines_cols() | |
| lines, cols = curses.LINES, curses.COLS | |
| new_lines = lines - 1 | |
| new_cols = cols + 1 | |
| curses.resizeterm(new_lines, new_cols) | |
| self.assertEqual(curses.LINES, new_lines) | |
| self.assertEqual(curses.COLS, new_cols) | |
| curses.resizeterm(lines, cols) | |
| self.assertEqual(curses.LINES, lines) | |
| self.assertEqual(curses.COLS, cols) | |
| with self.assertRaises(OverflowError): | |
| curses.resizeterm(35000, 1) | |
| with self.assertRaises(OverflowError): | |
| curses.resizeterm(1, 35000) | |
| # GH-120378: a failed resize can leave refresh broken; restore the | |
| # original size to recover. Avoid initscr(), which would switch away | |
| # from the shared newterm() screen and corrupt later tests. | |
| curses.resizeterm(lines, cols) | |
| self.stdscr.erase() | |
| def test_ungetch(self): | |
| curses.ungetch(b'A') | |
| self.assertEqual(self.stdscr.getkey(), 'A') | |
| curses.ungetch('B') | |
| self.assertEqual(self.stdscr.getkey(), 'B') | |
| curses.ungetch(67) | |
| self.assertEqual(self.stdscr.getkey(), 'C') | |
| def test_issue6243(self): | |
| curses.ungetch(1025) | |
| self.stdscr.getkey() | |
| @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 _storable for the character set, plus a non-BMP character. | |
| for ch in ('a', '\xe9', '\xa4', '\u20ac', '\u0454', '\U0010FFFF'): | |
| 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, 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 | |
| import codecs | |
| encoding = stdscr.encoding | |
| codecs.lookup(encoding) | |
| with self.assertRaises(TypeError): | |
| stdscr.encoding = 10 | |
| stdscr.encoding = encoding | |
| with self.assertRaises(TypeError): | |
| del stdscr.encoding | |
| @unittest.skipIf(MISSING_C_DOCSTRINGS, | |
| "Signature information for builtins requires docstrings") | |
| def test_issue21088(self): | |
| stdscr = self.stdscr | |
| # | |
| # http://bugs.python.org/issue21088 | |
| # | |
| # the bug: | |
| # when converting curses.window.addch to Argument Clinic | |
| # the first two parameters were switched. | |
| # if someday we can represent the signature of addch | |
| # we will need to rewrite this test. | |
| try: | |
| signature = inspect.signature(stdscr.addch) | |
| self.assertFalse(signature) | |
| except ValueError: | |
| # not generating a signature is fine. | |
| pass | |
| # So. No signature for addch. | |
| # But Argument Clinic gave us a human-readable equivalent | |
| # as the first line of the docstring. So we parse that, | |
| # and ensure that the parameters appear in the correct order. | |
| # Since this is parsing output from Argument Clinic, we can | |
| # be reasonably certain the generated parsing code will be | |
| # correct too. | |
| human_readable_signature = stdscr.addch.__doc__.split("\n")[0] | |
| self.assertIn("[y, x,]", human_readable_signature) | |
| @requires_curses_window_meth('resize') | |
| def test_issue13051(self): | |
| win = curses.newwin(5, 15, 2, 5) | |
| box = curses.textpad.Textbox(win, insert_mode=True) | |
| lines, cols = win.getmaxyx() | |
| win.resize(lines-2, cols-2) | |
| # this may cause infinite recursion, leading to a RuntimeError | |
| box._insert_printable_char('a') | |
| class MiscTests(unittest.TestCase): | |
| @requires_curses_func('update_lines_cols') | |
| def test_update_lines_cols(self): | |
| curses.update_lines_cols() | |
| lines, cols = curses.LINES, curses.COLS | |
| curses.LINES = curses.COLS = 0 | |
| curses.update_lines_cols() | |
| self.assertEqual(curses.LINES, lines) | |
| self.assertEqual(curses.COLS, cols) | |
| @requires_curses_func('ncurses_version') | |
| def test_ncurses_version(self): | |
| v = curses.ncurses_version | |
| if verbose: | |
| print(f'ncurses_version = {curses.ncurses_version}', flush=True) | |
| self.assertIsInstance(v[:], tuple) | |
| self.assertEqual(len(v), 3) | |
| self.assertIsInstance(v[0], int) | |
| self.assertIsInstance(v[1], int) | |
| self.assertIsInstance(v[2], int) | |
| self.assertIsInstance(v.major, int) | |
| self.assertIsInstance(v.minor, int) | |
| self.assertIsInstance(v.patch, int) | |
| self.assertEqual(v[0], v.major) | |
| self.assertEqual(v[1], v.minor) | |
| self.assertEqual(v[2], v.patch) | |
| self.assertGreaterEqual(v.major, 0) | |
| self.assertGreaterEqual(v.minor, 0) | |
| self.assertGreaterEqual(v.patch, 0) | |
| def test_has_extended_color_support(self): | |
| r = curses.has_extended_color_support() | |
| self.assertIsInstance(r, bool) | |
| def test_err_and_ok(self): | |
| # ERR is negative; it is not a chtype constant. | |
| self.assertEqual(curses.ERR, -1) | |
| self.assertEqual(curses.OK, 0) | |
| def test_type_names(self): | |
| # The curses types report their public module rather than the | |
| # underscore extension that implements them. | |
| for name in 'window', 'complexchar', 'complexstr', 'screen', 'error': | |
| tp = getattr(curses, name) | |
| self.assertEqual(tp.__module__, 'curses') | |
| self.assertEqual(tp.__qualname__, name) | |
| self.assertEqual(tp.__name__, name) | |
| @requires_curses_func('panel') | |
| def test_panel_type_names(self): | |
| import curses.panel | |
| for name in 'panel', 'error': | |
| tp = getattr(curses.panel, name) | |
| self.assertEqual(tp.__module__, 'curses.panel') | |
| self.assertEqual(tp.__qualname__, name) | |
| self.assertEqual(tp.__name__, name) | |
| class TestAscii(unittest.TestCase): | |
| def test_controlnames(self): | |
| for name in curses.ascii.controlnames: | |
| self.assertHasAttr(curses.ascii, name) | |
| def test_ctypes(self): | |
| def check(func, expected): | |
| with self.subTest(ch=c, func=func): | |
| self.assertEqual(func(i), expected) | |
| self.assertEqual(func(c), expected) | |
| for i in range(256): | |
| c = chr(i) | |
| b = bytes([i]) | |
| check(curses.ascii.isalnum, b.isalnum()) | |
| check(curses.ascii.isalpha, b.isalpha()) | |
| check(curses.ascii.isdigit, b.isdigit()) | |
| check(curses.ascii.islower, b.islower()) | |
| check(curses.ascii.isspace, b.isspace()) | |
| check(curses.ascii.isupper, b.isupper()) | |
| check(curses.ascii.isascii, i < 128) | |
| check(curses.ascii.ismeta, i >= 128) | |
| check(curses.ascii.isctrl, i < 32) | |
| check(curses.ascii.iscntrl, i < 32 or i == 127) | |
| check(curses.ascii.isblank, c in ' \t') | |
| check(curses.ascii.isgraph, 32 < i <= 126) | |
| check(curses.ascii.isprint, 32 <= i <= 126) | |
| check(curses.ascii.ispunct, c in string.punctuation) | |
| check(curses.ascii.isxdigit, c in string.hexdigits) | |
| for i in (-2, -1, 256, sys.maxunicode, sys.maxunicode+1): | |
| self.assertFalse(curses.ascii.isalnum(i)) | |
| self.assertFalse(curses.ascii.isalpha(i)) | |
| self.assertFalse(curses.ascii.isdigit(i)) | |
| self.assertFalse(curses.ascii.islower(i)) | |
| self.assertFalse(curses.ascii.isspace(i)) | |
| self.assertFalse(curses.ascii.isupper(i)) | |
| self.assertFalse(curses.ascii.isascii(i)) | |
| self.assertFalse(curses.ascii.isctrl(i)) | |
| self.assertFalse(curses.ascii.iscntrl(i)) | |
| self.assertFalse(curses.ascii.isblank(i)) | |
| self.assertFalse(curses.ascii.isgraph(i)) | |
| self.assertFalse(curses.ascii.isprint(i)) | |
| self.assertFalse(curses.ascii.ispunct(i)) | |
| self.assertFalse(curses.ascii.isxdigit(i)) | |
| self.assertFalse(curses.ascii.ismeta(-1)) | |
| def test_ascii(self): | |
| ascii = curses.ascii.ascii | |
| self.assertEqual(ascii('\xc1'), 'A') | |
| self.assertEqual(ascii('A'), 'A') | |
| self.assertEqual(ascii(ord('\xc1')), ord('A')) | |
| def test_ctrl(self): | |
| ctrl = curses.ascii.ctrl | |
| self.assertEqual(ctrl('J'), '\n') | |
| self.assertEqual(ctrl('\n'), '\n') | |
| self.assertEqual(ctrl('@'), '\0') | |
| self.assertEqual(ctrl(ord('J')), ord('\n')) | |
| # A non-ASCII argument is returned unchanged (no control character). | |
| self.assertEqual(ctrl('\xe9'), '\xe9') | |
| self.assertEqual(ctrl(0xe9), 0xe9) | |
| def test_alt(self): | |
| alt = curses.ascii.alt | |
| self.assertEqual(alt('\n'), '\x8a') | |
| self.assertEqual(alt('A'), '\xc1') | |
| self.assertEqual(alt(ord('A')), 0xc1) | |
| def test_unctrl(self): | |
| unctrl = curses.ascii.unctrl | |
| self.assertEqual(unctrl('a'), 'a') | |
| self.assertEqual(unctrl('A'), 'A') | |
| self.assertEqual(unctrl(';'), ';') | |
| self.assertEqual(unctrl(' '), ' ') | |
| self.assertEqual(unctrl('\x7f'), '^?') | |
| self.assertEqual(unctrl('\n'), '^J') | |
| self.assertEqual(unctrl('\0'), '^@') | |
| self.assertEqual(unctrl(ord('A')), 'A') | |
| self.assertEqual(unctrl(ord('\n')), '^J') | |
| # Meta-bit characters | |
| self.assertEqual(unctrl('\x8a'), '!^J') | |
| self.assertEqual(unctrl('\xc1'), '!A') | |
| self.assertEqual(unctrl(ord('\x8a')), '!^J') | |
| self.assertEqual(unctrl(ord('\xc1')), '!A') | |
| @unittest.skipUnless(hasattr(curses, 'complexchar'), | |
| 'requires the curses.complexchar type') | |
| def test_complexchar(self): | |
| # The predicates, ctrl() and unctrl() accept a complexchar too, using | |
| # its single character. A narrow build just forms fewer cells. | |
| cc = curses.complexchar | |
| def storable(s): | |
| # ValueError if s has combining marks on a narrow build. | |
| # OverflowError if s is a multibyte character on a narrow build. | |
| try: | |
| cc(s) | |
| except (ValueError, OverflowError): | |
| return False | |
| return True | |
| self.assertTrue(curses.ascii.isupper(cc('A'))) | |
| self.assertTrue(curses.ascii.isalpha(cc('A', curses.A_BOLD))) | |
| self.assertFalse(curses.ascii.isdigit(cc('A'))) | |
| self.assertTrue(curses.ascii.isdigit(cc('7'))) | |
| self.assertTrue(curses.ascii.iscntrl(cc('\n'))) | |
| self.assertEqual(curses.ascii.ctrl(cc('J')), '\n') | |
| self.assertEqual(curses.ascii.unctrl(cc('\n')), '^J') | |
| self.assertEqual(curses.ascii.unctrl(cc('A')), 'A') | |
| # A non-ASCII character: classified by code point, no control character. | |
| if storable('\xe9'): | |
| self.assertFalse(curses.ascii.isascii(cc('\xe9'))) | |
| self.assertTrue(curses.ascii.ismeta(cc('\xe9'))) | |
| self.assertEqual(curses.ascii.ctrl(cc('\xe9')), '\xe9') | |
| # A cell with combining marks is not a single character, so no | |
| # predicate matches it (needs a wide build to store). | |
| if storable('e\u0301'): | |
| self.assertFalse(curses.ascii.isalpha(cc('e\u0301'))) | |
| self.assertFalse(curses.ascii.isascii(cc('e\u0301'))) | |
| def lorem_ipsum(win): | |
| text = [ | |
| 'Lorem ipsum', | |
| 'dolor sit amet,', | |
| 'consectetur', | |
| 'adipiscing elit,', | |
| 'sed do eiusmod', | |
| 'tempor incididunt', | |
| 'ut labore et', | |
| 'dolore magna', | |
| 'aliqua.', | |
| ] | |
| maxy, maxx = win.getmaxyx() | |
| for y, line in enumerate(text[:maxy]): | |
| win.addstr(y, 0, line[:maxx - (y == maxy - 1)]) | |
| class TextboxTest(unittest.TestCase): | |
| def setUp(self): | |
| self.mock_win = MagicMock(spec=curses.window) | |
| self.mock_win.getyx.return_value = (1, 1) | |
| self.mock_win.getmaxyx.return_value = (10, 20) | |
| self.mock_win.encoding = 'utf-8' | |
| # A non-blank cell so that _end_of_line() reports a full line: instr() | |
| # backs the text reads, inch() the insert-mode shift. | |
| self.mock_win.instr.return_value = b'x' | |
| self.mock_win.inch.return_value = ord('x') | |
| self.textbox = curses.textpad.Textbox(self.mock_win) | |
| def test_init(self): | |
| """Test textbox initialization.""" | |
| self.mock_win.reset_mock() | |
| tb = curses.textpad.Textbox(self.mock_win) | |
| self.mock_win.getmaxyx.assert_called_once_with() | |
| self.mock_win.keypad.assert_called_once_with(1) | |
| self.assertEqual(tb.insert_mode, False) | |
| self.assertEqual(tb.stripspaces, 1) | |
| self.assertIsNone(tb.lastcmd) | |
| self.mock_win.reset_mock() | |
| def test_insert(self): | |
| """Test inserting a printable character.""" | |
| self.mock_win.reset_mock() | |
| # An integer keystroke is decoded to text: addch() would take it as a | |
| # code point on a wide build. | |
| for ch in 'abc': | |
| self.textbox.do_command(ord(ch)) | |
| self.mock_win.addch.assert_called_with(ch, 0) | |
| self.mock_win.reset_mock() | |
| def test_delete(self): | |
| """Test deleting a character.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.ascii.BS) | |
| self.textbox.do_command(curses.KEY_BACKSPACE) | |
| self.textbox.do_command(curses.ascii.DEL) | |
| assert self.mock_win.delch.call_count == 3 | |
| self.mock_win.reset_mock() | |
| def test_move_left(self): | |
| """Test moving the cursor left.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.KEY_LEFT) | |
| self.mock_win.move.assert_called_with(1, 0) | |
| self.mock_win.reset_mock() | |
| def test_move_right(self): | |
| """Test moving the cursor right.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.KEY_RIGHT) | |
| self.mock_win.move.assert_called_with(1, 2) | |
| self.mock_win.reset_mock() | |
| def test_move_left_and_right(self): | |
| """Test moving the cursor left and then right.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.KEY_LEFT) | |
| self.mock_win.move.assert_called_with(1, 0) | |
| self.textbox.do_command(curses.KEY_RIGHT) | |
| self.mock_win.move.assert_called_with(1, 2) | |
| self.mock_win.reset_mock() | |
| def test_move_up(self): | |
| """Test moving the cursor up.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.KEY_UP) | |
| self.mock_win.move.assert_called_with(0, 1) | |
| self.mock_win.reset_mock() | |
| def test_move_down(self): | |
| """Test moving the cursor down.""" | |
| self.mock_win.reset_mock() | |
| self.textbox.do_command(curses.KEY_DOWN) | |
| self.mock_win.move.assert_called_with(2, 1) | |
| self.mock_win.reset_mock() | |
| class NewtermTestBase(unittest.TestCase): | |
| # Shared plumbing for tests that drive newterm() over their own | |
| # pseudo-terminal(s). newterm()/set_term() mutate global curses state, but | |
| # each test never touches the screen shared by TestCurses, whose setUp() | |
| # makes that screen current again. So these can run in this process, | |
| # without a real terminal and without a subprocess. | |
| def setUp(self): | |
| # newterm() may install signal handlers; restore them afterwards. | |
| self.save_signals = SaveSignals() | |
| self.save_signals.save() | |
| self.addCleanup(self.save_signals.restore) | |
| # A new pseudo-terminal has no size, so newterm() takes it from | |
| # $LINES and $COLUMNS, or from the terminfo entry if they are unset. | |
| env = self.enterContext(os_helper.EnvironmentVarGuard()) | |
| env['LINES'] = '24' | |
| env['COLUMNS'] = '80' | |
| def tearDown(self): | |
| # Leave visual mode and reclaim the test's screens while their | |
| # pseudo-terminals are still open (make_pty() closes them later). | |
| try: | |
| curses.endwin() | |
| except curses.error: | |
| pass | |
| gc_collect() | |
| @staticmethod | |
| def _drain_pty(master, stop): | |
| # Read and discard whatever curses writes to the screen, until asked to | |
| # stop and nothing more is pending. poll() rather than a blocking | |
| # read() so we can stop without closing the fd (closing it while this | |
| # thread is blocked in read() hangs on macOS). | |
| poller = select.poll() | |
| poller.register(master, select.POLLIN) | |
| while True: | |
| if poller.poll(100): | |
| try: | |
| if not os.read(master, 1024): | |
| break # EOF | |
| except OSError: | |
| break | |
| elif stop.is_set(): | |
| break | |
| def make_pty(self): | |
| master, slave = os.openpty() | |
| # Nothing reads the master end, so writing to the slave and the | |
| # tcdrain() in endwin() can block on macOS once the pty buffer fills; | |
| # drain it from a background thread (endwin() releases the GIL). | |
| stop = threading.Event() | |
| reader = threading.Thread(target=self._drain_pty, args=(master, stop), | |
| daemon=True) | |
| reader.start() | |
| # Stop and join the reader before closing the fds: on macOS, closing | |
| # either end while the reader is blocked in read() hangs. | |
| def stop_reader(): | |
| stop.set() | |
| reader.join(SHORT_TIMEOUT) | |
| self.addCleanup(os.close, master) | |
| self.addCleanup(os.close, slave) | |
| self.addCleanup(stop_reader) | |
| return slave | |
| @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') | |
| @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') | |
| @unittest.skipIf(not term or term == 'unknown', | |
| f"$TERM={term!r}, newterm() may not work") | |
| @unittest.skipIf(sys.platform == "cygwin", | |
| "cygwin's curses mostly just hangs") | |
| class ScreenTests(NewtermTestBase): | |
| def test_newterm(self): | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| self.assertIsInstance(screen, curses.screen) | |
| win = screen.stdscr | |
| self.assertIsInstance(win, curses.window) | |
| self.assertEqual(win.getmaxyx(), (24, 80)) | |
| win.addstr(0, 0, 'hello') | |
| win.refresh() | |
| def test_lines_columns_env(self): | |
| # $LINES and $COLUMNS override the size from the terminfo entry | |
| # (24x80 for xterm). | |
| for lines, columns, size in [('30', '100', (30, 100)), | |
| ('30', None, (30, 80)), | |
| (None, '100', (24, 100)), | |
| (None, None, (24, 80))]: | |
| with self.subTest(lines=lines, columns=columns): | |
| with os_helper.EnvironmentVarGuard() as env: | |
| for name, value in [('LINES', lines), | |
| ('COLUMNS', columns)]: | |
| if value is None: | |
| env.unset(name) | |
| else: | |
| env[name] = value | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| self.assertEqual(screen.stdscr.getmaxyx(), size) | |
| self.assertEqual((curses.LINES, curses.COLS), size) | |
| def test_newterm_file_object(self): | |
| # type=None uses $TERM; the file arguments accept file objects too. | |
| s = self.make_pty() | |
| out = os.fdopen(os.dup(s), 'wb', buffering=0) | |
| self.addCleanup(out.close) | |
| screen = curses.newterm(None, out, s) | |
| self.assertIsInstance(screen, curses.screen) | |
| def test_set_term(self): | |
| s = self.make_pty() | |
| s2 = self.make_pty() | |
| a = curses.newterm('xterm', s, s) # current screen is a | |
| b = curses.newterm('xterm', s2, s2) # current screen is b | |
| self.assertIs(curses.set_term(a), b) # returns the previous one | |
| self.assertIs(curses.set_term(b), a) | |
| def make_screens_of_different_terms(self): | |
| # Two screens of different sizes and numbers of colors. Return them | |
| # with the values of LINES, COLS, COLORS and COLOR_PAIRS for each. | |
| screens = [] | |
| with os_helper.EnvironmentVarGuard() as env: | |
| for term, lines, cols in [('xterm', 25, 80), | |
| ('xterm-256color', 30, 100)]: | |
| env['LINES'] = str(lines) | |
| env['COLUMNS'] = str(cols) | |
| s = self.make_pty() | |
| try: | |
| screen = curses.newterm(term, s, s) | |
| except curses.error: | |
| self.skipTest(f'no {term} terminfo entry') | |
| curses.start_color() | |
| self.assertEqual(screen.stdscr.getmaxyx(), (lines, cols)) | |
| screens.append((screen, self.get_screen_values())) | |
| self.assertNotEqual(screens[0][1], screens[1][1]) | |
| return screens | |
| def get_screen_values(self): | |
| return curses.LINES, curses.COLS, curses.COLORS, curses.COLOR_PAIRS | |
| def test_set_term_refreshes_lines_cols_colors(self): | |
| (a, avalues), (b, bvalues) = self.make_screens_of_different_terms() | |
| self.assertIs(curses.set_term(a), b) | |
| self.assertEqual(self.get_screen_values(), avalues) | |
| self.assertIs(curses.set_term(b), a) | |
| self.assertEqual(self.get_screen_values(), bvalues) | |
| @unittest.skipUnless(hasattr(curses.screen, 'use'), | |
| 'requires curses.screen.use()') | |
| def test_use_screen_refreshes_lines_cols_colors(self): | |
| (a, avalues), (b, bvalues) = self.make_screens_of_different_terms() | |
| self.assertEqual(a.use(lambda scr: self.get_screen_values()), avalues) | |
| self.assertEqual(self.get_screen_values(), bvalues) | |
| def raise_in_use(scr): | |
| self.assertEqual(self.get_screen_values(), avalues) | |
| raise CustomError | |
| with self.assertRaises(CustomError): | |
| a.use(raise_in_use) | |
| self.assertEqual(self.get_screen_values(), bvalues) | |
| def test_window_keeps_screen_alive(self): | |
| # The standard window keeps its screen alive; dropping every other | |
| # reference and collecting must not invalidate the window. | |
| s = self.make_pty() | |
| win = curses.newterm('xterm', s, s).stdscr | |
| gc_collect() | |
| win.addstr(0, 0, 'still alive') | |
| win.refresh() | |
| @unittest.skipUnless(hasattr(curses.screen, 'use'), | |
| 'requires curses.screen.use()') | |
| def test_window_made_in_use_keeps_its_screen_alive(self): | |
| # use() makes its screen current for the callback, so a window created | |
| # there belongs to that screen and must keep it alive, not the screen | |
| # that was current before. | |
| s = self.make_pty() | |
| s2 = self.make_pty() | |
| a = curses.newterm('xterm', s, s) | |
| b = curses.newterm('xterm', s2, s2) # current screen is b | |
| win = a.use(lambda scr: curses.newwin(3, 3)) | |
| del a | |
| gc_collect() | |
| win.addstr(0, 0, 'x') | |
| b.stdscr.refresh() | |
| @unittest.skipUnless(hasattr(curses.screen, 'use'), | |
| 'requires curses.screen.use()') | |
| def test_initscr_in_use_returns_its_screen(self): | |
| # initscr() returns the standard window of the current screen, and | |
| # inside use() that is the used screen. | |
| s = self.make_pty() | |
| s2 = self.make_pty() | |
| a = curses.newterm('xterm', s, s) | |
| b = curses.newterm('xterm', s2, s2) # current screen is b | |
| self.assertIs(a.use(lambda scr: curses.initscr()), a.stdscr) | |
| def test_screen_freed(self): | |
| # Dropping all references to a (non-current) screen and its windows | |
| # frees it without error. | |
| s = self.make_pty() | |
| s2 = self.make_pty() | |
| a = curses.newterm('xterm', s, s) | |
| b = curses.newterm('xterm', s2, s2) # a is no longer current | |
| del a | |
| gc_collect() | |
| def test_close(self): | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| win = screen.stdscr | |
| self.assertIsInstance(win, curses.window) | |
| screen.close() | |
| # After close() the standard window is detached and unusable, and | |
| # stdscr is None. No reference cycle remains. | |
| self.assertIsNone(screen.stdscr) | |
| self.assertRaises(curses.error, win.addstr, 0, 0, 'x') | |
| # close() is idempotent. | |
| screen.close() | |
| def test_close_then_write_with_attr_keeps_no_reference(self): | |
| # A write with an *attr* argument on a detached window fails while | |
| # setting the rendition, and has to release the bytes it converted. | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| win = screen.stdscr | |
| screen.close() | |
| writes = [lambda b: win.addstr(b, curses.A_BOLD), | |
| lambda b: win.addnstr(b, 4, curses.A_BOLD), | |
| lambda b: win.insstr(b, curses.A_BOLD), | |
| lambda b: win.insnstr(b, 4, curses.A_BOLD)] | |
| data = b'x' * 8 | |
| nrefs = sys.getrefcount(data) | |
| for write in writes: | |
| self.assertRaises(curses.error, write, data) | |
| self.assertEqual(sys.getrefcount(data), nrefs) | |
| @requires_curses_func('panel') | |
| def test_close_then_panel_replace(self): | |
| # A detached window has no underlying curses window, so replace() | |
| # must reject it. It used to be accepted, and the panel then | |
| # crashed inside curses on its next use. | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| win = screen.stdscr | |
| panel = curses.panel.new_panel(curses.newwin(3, 6, 0, 0)) | |
| # Drop the panel from the global stack before later tests inspect it. | |
| self.addCleanup(gc_collect) | |
| screen.close() | |
| self.assertRaises(curses.panel.error, panel.replace, win) | |
| # The panel kept its own window, so it still works. | |
| panel.move(1, 1) | |
| @unittest.skipUnless(hasattr(curses, 'new_prescr'), | |
| 'requires curses.new_prescr()') | |
| def test_new_prescr(self): | |
| screen = curses.new_prescr() | |
| self.assertIsInstance(screen, curses.screen) | |
| self.assertIsNone(screen.stdscr) | |
| del screen | |
| gc_collect() | |
| @requires_curses_func('new_prescr') | |
| def test_set_term_prescr_screen(self): | |
| # A new_prescr() screen has no terminal, so it cannot become the | |
| # current one. It used to be accepted, and the next refresh then | |
| # crashed inside curses. | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| self.assertRaises(curses.error, curses.set_term, curses.new_prescr()) | |
| # The current screen is unchanged, so refreshing it still works. | |
| screen.stdscr.refresh() | |
| @unittest.skipUnless(hasattr(curses, 'new_prescr'), | |
| 'requires curses.new_prescr()') | |
| @unittest.skipUnless(hasattr(curses.screen, 'use'), | |
| 'requires curses.screen.use()') | |
| def test_use_prescr_screen(self): | |
| # use() makes its screen current for the callback, so a new_prescr() | |
| # screen is current there without having a terminal. Operations that | |
| # need one used to crash inside curses. | |
| s = self.make_pty() | |
| screen = curses.newterm('xterm', s, s) | |
| prescr = curses.new_prescr() | |
| for func in [ | |
| lambda scr: curses.doupdate(), | |
| lambda scr: curses.newwin(3, 3), | |
| lambda scr: screen.stdscr.refresh(), | |
| lambda scr: screen.stdscr.getch(), | |
| ]: | |
| with self.assertRaises(curses.error): | |
| prescr.use(func) | |
| # Affecting the state before initscr() is what such a screen is for. | |
| # use_env() is process-wide, not a property of this screen. | |
| self.addCleanup(curses.use_env, True) | |
| prescr.use(lambda scr: curses.use_env(False)) | |
| # The current screen is unchanged. | |
| screen.stdscr.refresh() | |
| @unittest.skipUnless(hasattr(curses, 'new_prescr'), | |
| 'requires curses.new_prescr()') | |
| def test_new_prescr_returns_existing_screen(self): | |
| pre1 = curses.new_prescr() | |
| pre2 = curses.new_prescr() | |
| self.assertIs(pre1, pre2) | |
| @unittest.skipUnless(hasattr(curses, 'new_prescr'), | |
| 'requires curses.new_prescr()') | |
| def test_newterm_after_new_prescr_keeps_screen_alive(self): | |
| # newterm() adopts the SCREEN created by new_prescr(). Dropping the | |
| # pre-screen wrapper must not delete the live screen. | |
| s = self.make_pty() | |
| pre = curses.new_prescr() | |
| screen = curses.newterm('xterm', s, s) | |
| del pre | |
| gc_collect() | |
| screen.stdscr.addstr(0, 0, 'x') | |
| screen.stdscr.refresh() | |
| @unittest.skipUnless(hasattr(curses, 'new_prescr'), | |
| 'requires curses.new_prescr()') | |
| def test_initscr_after_new_prescr_keeps_screen_alive(self): | |
| # initscr() adopts the SCREEN created by new_prescr(). Dropping the | |
| # pre-screen wrapper must not delete the live screen. | |
| s = self.make_pty() | |
| saved = os.dup(1) | |
| self.addCleanup(os.close, saved) | |
| self.addCleanup(os.dup2, saved, 1) | |
| os.dup2(s, 1) | |
| pre = curses.new_prescr() | |
| stdscr = curses.initscr() | |
| del pre | |
| gc_collect() | |
| stdscr.addstr(0, 0, 'x') | |
| stdscr.refresh() | |
| def test_initscr_after_newterm_keeps_screen_alive(self): | |
| # initscr() called while a newterm() screen is current returns that | |
| # screen's own standard window, so the window keeps the screen alive. | |
| # It used to be a second wrapper created without a screen: using it | |
| # after the screen was collected read freed memory, and both wrappers | |
| # could delwin() the same window. | |
| s1 = self.make_pty() | |
| s2 = self.make_pty() | |
| screen1 = curses.newterm('xterm', s1, s1) | |
| screen2 = curses.newterm('xterm', s2, s2) | |
| curses.set_term(screen1) | |
| win = curses.initscr() | |
| self.assertIs(win, screen1.stdscr) | |
| curses.set_term(screen2) | |
| del screen1 | |
| gc_collect() | |
| win.addstr(0, 0, 'x') | |
| @cpython_only | |
| def test_disallow_instantiation(self): | |
| # The screen type cannot be instantiated directly (bpo-43916). | |
| check_disallow_instantiation(self, curses.screen) | |
| @unittest.skipUnless(hasattr(curses, 'slk_init'), 'requires curses.slk_init()') | |
| @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') | |
| @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') | |
| @unittest.skipIf(not term or term == 'unknown', | |
| f"$TERM={term!r}, newterm() may not work") | |
| @unittest.skipIf(sys.platform == "cygwin", | |
| "cygwin's curses mostly just hangs") | |
| class SLKTests(NewtermTestBase): | |
| # Soft-label keys reserve the bottom screen line for a row of labels. | |
| # slk_init() must run before newterm()/initscr(), so each test sets up its | |
| # own screen rather than reusing the one TestCurses builds in setUp(). | |
| def make_slk_screen(self, fmt=0, term='xterm'): | |
| s = self.make_pty() | |
| curses.slk_init(fmt) | |
| return curses.newterm(term, s, s) | |
| def test_init_reserves_a_line(self): | |
| # Every layout takes the bottom line for the labels; the index-line | |
| # layout (3) takes a second line for the index. Layouts 0 and 1 are | |
| # standard; 2 and 3 are ncurses extensions that other curses | |
| # implementations reject (slk_init() then returns an error). | |
| ncurses = hasattr(curses, 'ncurses_version') | |
| for fmt, lines in [(0, 23), (1, 23), (2, 23), (3, 22)]: | |
| with self.subTest(fmt=fmt): | |
| try: | |
| screen = self.make_slk_screen(fmt) | |
| except curses.error: | |
| if ncurses or fmt < 2: | |
| raise | |
| continue | |
| self.assertEqual(screen.stdscr.getmaxyx()[0], lines) | |
| curses.endwin() | |
| def test_init_bad_format(self): | |
| for fmt in (-1, 4): | |
| self.assertRaises(ValueError, curses.slk_init, fmt) | |
| def test_set_and_label(self): | |
| self.make_slk_screen() | |
| curses.slk_set(1, 'Help', 0) | |
| curses.slk_set(2, 'Save', 1) | |
| curses.slk_set(3, 'Quit', 2) | |
| self.assertEqual(curses.slk_label(1), 'Help') | |
| self.assertEqual(curses.slk_label(2), 'Save') | |
| self.assertEqual(curses.slk_label(3), 'Quit') | |
| def test_set_wide(self): | |
| screen = self.make_slk_screen() | |
| label = 'Ångström' | |
| if not storable(label, screen.stdscr.encoding): | |
| self.skipTest('cannot store %r in this locale' % label) | |
| curses.slk_set(1, label, 0) | |
| # The label can be truncated to fit the soft label width, e.g. in the | |
| # EUC-JP locale, where "Å" and "ö" are double-width JIS X 0212 | |
| # characters, so the 8-column label only fits "Ångstr". | |
| self.assertIn(curses.slk_label(1), (label, 'Ångstr')) | |
| def test_set_bad_justify(self): | |
| self.make_slk_screen() | |
| for justify in (-1, 3): | |
| self.assertRaises(ValueError, curses.slk_set, 1, 'x', justify) | |
| def test_refresh(self): | |
| self.make_slk_screen() | |
| curses.slk_set(1, 'Help', 0) | |
| curses.slk_noutrefresh() | |
| curses.slk_refresh() | |
| curses.slk_clear() | |
| curses.slk_restore() | |
| curses.slk_touch() | |
| def test_attributes(self): | |
| self.make_slk_screen() | |
| curses.slk_attron(curses.A_BOLD) | |
| curses.slk_attrset(curses.A_UNDERLINE) | |
| curses.slk_attroff(curses.A_BOLD) | |
| if hasattr(curses, 'slk_attr'): | |
| self.assertIsInstance(curses.slk_attr(), int) | |
| def test_attr_on_off(self): | |
| self.make_slk_screen() | |
| curses.slk_attr_on(curses.A_BOLD) | |
| curses.slk_attr_off(curses.A_BOLD) | |
| def test_color(self): | |
| # slk_attr_set() and slk_color() act on a color pair, so the color | |
| # subsystem must be started first. | |
| self.make_slk_screen() | |
| if not curses.has_colors(): | |
| self.skipTest('requires colors support') | |
| curses.start_color() | |
| curses.slk_attr_set(curses.A_BOLD) | |
| curses.slk_attr_set(curses.A_BOLD, 0) | |
| curses.slk_color(0) | |
| def test_color_wide_pair(self): | |
| # Drive a terminal with enough color pairs to reach past a short, | |
| # rather than relying on whatever $TERM happens to be. | |
| try: | |
| self.make_slk_screen(term='xterm-256color') | |
| except curses.error: | |
| self.skipTest('no xterm-256color terminfo entry') | |
| if not curses.has_colors(): | |
| self.skipTest('requires colors support') | |
| curses.start_color() | |
| if not (curses.has_extended_color_support() | |
| and curses.COLOR_PAIRS > SHORT_MAX + 1): | |
| self.skipTest('requires extended color support') | |
| # A pair that does not fit in a short is still a valid pair here. | |
| curses.slk_color(SHORT_MAX + 1) | |
| # The low 16 bits of this are pair 5, but the pair itself is out of | |
| # range, so it must raise instead of selecting pair 5. | |
| self.assertRaises(curses.error, curses.slk_color, | |
| curses.COLOR_PAIRS * 2 + 5) | |
| @unittest.skipUnless(hasattr(curses, 'newterm'), 'requires curses.newterm()') | |
| @unittest.skipIf(BROKEN_NEWTERM, 'ncurses < 6.5 mishandles repeated newterm()') | |
| @unittest.skipIf(not term or term == 'unknown', | |
| f"$TERM={term!r}, newterm() may not work") | |
| @unittest.skipIf(sys.platform == "cygwin", | |
| "cygwin's curses mostly just hangs") | |
| class TermAttrsTests(NewtermTestBase): | |
| # A_ITALIC is the topmost bit of a 32-bit attribute mask, so termattrs() | |
| # only tells a signed result from an unsigned one on a terminal that | |
| # advertises it. Drive a known terminal type over a pseudo-terminal | |
| # instead of relying on whatever $TERM happens to be. | |
| def test_termattrs_is_not_negative(self): | |
| s = self.make_pty() | |
| try: | |
| curses.newterm('xterm-256color', s, s) | |
| except curses.error: | |
| self.skipTest('no xterm-256color terminfo entry') | |
| attrs = curses.termattrs() | |
| italic = getattr(curses, 'A_ITALIC', 0) | |
| if not italic or not attrs & italic: | |
| self.skipTest('the terminal advertises no attribute in the top bit') | |
| self.assertGreaterEqual(attrs, 0) | |
| # termattrs() exists to be passed back to the attribute functions, | |
| # which reject a negative mask. | |
| curses.newwin(1, 1).attrset(attrs) | |
| if __name__ == '__main__': | |
| unittest.main() |