-
-
Notifications
You must be signed in to change notification settings - Fork 35k
Expand file tree
/
Copy pathtest_curses.py
More file actions
3175 lines (2841 loc) · 130 KB
/
Copy pathtest_curses.py
File metadata and controls
3175 lines (2841 loc) · 130 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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
def _wide_build():
# True on a build that stores wide-character cells (built against ncursesw).
# A wide build accepts a spacing character plus a combining mark in a single
# cell; a narrow build accepts only one character per cell. This stays a
# reliable wide/narrow signal even as the wide-character functions (get_wch()
# and friends) become available on narrow builds too, because the
# multi-codepoint cell capacity itself is build-specific.
if not hasattr(curses, 'complexchar'):
return hasattr(curses.window, 'get_wch')
try:
curses.complexchar('e\u0301') # 'e' + combining acute: two code points
except ValueError:
return False
return True
WIDE_BUILD = _wide_build()
def requires_wide_build(test):
@functools.wraps(test)
def wrapped(self, *args, **kwargs):
if not WIDE_BUILD:
raise unittest.SkipTest('requires a wide-character curses build')
test(self, *args, **kwargs)
return wrapped
def requires_colors(test):
@functools.wraps(test)
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_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 _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 integer and wide-string characters.
self.assertRaises(TypeError, stdscr.box, vline, ord('-'))
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)
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)
# 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.assertTrue(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')
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 (a byte > 127). The
# cell is read back with _read_char(), not inch(): on a wide build the
# int is stored through the locale as a wide character that inch()
# cannot represent for a character outside Latin-1.
for c in ('é', '¤', '€', 'є'):
try:
b = c.encode(encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
continue
v = b[0]
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_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 ')
self.assertRaises(ValueError, stdscr.instr, -2)
self.assertRaises(ValueError, stdscr.instr, 0, 2, -2)
# A non-ASCII character of an 8-bit locale reads back as its encoded
# byte (see _encodable for the set). Both instr() and inch() return the
# locale byte for any character that fits the locale's single-byte
# encoding.
encoding = stdscr.encoding
for ch in ('A', 'é', '¤', '€', 'є'):
try:
b = ch.encode(encoding)
except UnicodeEncodeError:
continue
if len(b) != 1:
continue
with self.subTest(ch=ch):
stdscr.addstr(2, 0, ch)
self.assertEqual(stdscr.instr(2, 0, 1), b)
self.assertEqual(stdscr.inch(2, 0) & curses.A_CHARTEXT, b[0])
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)