| 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 |
| |
| # 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 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() |
| |
| # 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): |
| # Wide characters are only supported in a locale that can encode them. |
| try: |
| s.encode(self.stdscr.encoding) |
| except UnicodeEncodeError: |
| return False |
| return True |
| |
| def _storable(self, s): |
| # 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 self._encodable(s): |
| return False |
| if WIDE_BUILD: |
| return True |
| return len(s.encode(self.stdscr.encoding)) == len(s) |
| |
| 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') |
| # 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') |
| self.assertEqual(win.in_wch(0, 0), cell) |
| # 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) |
| |
| @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) |
| |
| 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_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 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() |
| |
| @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. |
| prescr.use(lambda scr: curses.use_env(False)) |
| # The current screen is unchanged. |
| screen.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): |
| s = self.make_pty() |
| curses.slk_init(fmt) |
| return curses.newterm('xterm', 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' |
| try: |
| label.encode(screen.stdscr.encoding) |
| except UnicodeEncodeError: |
| self.skipTest('the locale cannot encode %r' % 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) |
| |
| |
| @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() |