@@ -253,6 +253,69 @@ def test_refresh_control(self):
253253 self .assertIs (win .is_wintouched (), syncok )
254254 self .assertIs (stdscr .is_wintouched (), syncok )
255255
256+ @requires_curses_window_meth ('get_wch' )
257+ def test_addch_combining (self ):
258+ # A character cell may hold a spacing char plus combining marks.
259+ stdscr = self .stdscr
260+ stdscr .move (0 , 0 )
261+ stdscr .addch ('e\u0301 ' ) # 'e' + COMBINING ACUTE ACCENT
262+ stdscr .addch (1 , 0 , 'a\u0323 \u0300 ' ) # base plus two combining marks
263+ # Too many code points to fit in a single character cell.
264+ self .assertRaises (TypeError , stdscr .addch , 'e' + '\u0301 ' * 10 )
265+ # Only the first code point may be a spacing character.
266+ self .assertRaises (ValueError , stdscr .addch , 'ab' )
267+ self .assertRaises (ValueError , stdscr .addch , 'a\u0301 b' )
268+ # A lone control character is allowed (like addch(ord('\n'))), but it
269+ # cannot be combined with other characters, as base or otherwise.
270+ stdscr .addch ('\n ' )
271+ self .assertRaises (ValueError , stdscr .addch , 'a\n ' )
272+ self .assertRaises (ValueError , stdscr .addch , '\n \u0301 ' )
273+ self .assertRaises (ValueError , stdscr .addch , '\n e\u0301 ' )
274+
275+ @requires_curses_window_meth ('get_wch' )
276+ def test_addch_emoji (self ):
277+ # curses has no grapheme-cluster support: a cell holds one spacing
278+ # character plus zero-width combining characters. A lone emoji fits,
279+ # as does an emoji with a zero-width variation selector.
280+ stdscr = self .stdscr
281+ stdscr .addch (0 , 0 , '\U0001f600 ' ) # single emoji
282+ stdscr .addch (1 , 0 , '\u263a \ufe0f ' ) # WHITE SMILING FACE + VS-16
283+ # An emoji ZWJ sequence or an emoji with a modifier is more than one
284+ # spacing character and cannot share a single cell.
285+ self .assertRaises (ValueError , stdscr .addch ,
286+ '\U0001f44d \U0001f3fd ' ) # thumbs up + skin tone
287+ self .assertRaises (ValueError , stdscr .addch ,
288+ '\U0001f468 \u200d \U0001f469 ' ) # man ZWJ woman
289+
290+ @requires_curses_window_meth ('get_wch' )
291+ def test_wide_characters (self ):
292+ # Wide and combining characters in the character-cell methods.
293+ stdscr = self .stdscr
294+ combining = 'e\u0301 ' # 'e' + COMBINING ACUTE ACCENT
295+ vline , hline = '\u2502 ' , '\u2500 ' # box-drawing vertical/horizontal
296+ stdscr .move (0 , 0 )
297+ stdscr .echochar (combining )
298+ stdscr .insch (1 , 0 , combining )
299+ stdscr .hline (2 , 0 , hline , 5 )
300+ stdscr .vline (3 , 0 , vline , 3 )
301+ stdscr .bkgdset (combining )
302+ stdscr .bkgd (combining )
303+ stdscr .border (vline , vline , hline , hline )
304+ stdscr .box (vline , hline )
305+ # border() and box() cannot mix integer and wide-string characters.
306+ self .assertRaises (TypeError , stdscr .box , vline , ord ('-' ))
307+
308+
309+ @requires_curses_window_meth ('in_wstr' )
310+ def test_in_wstr (self ):
311+ # The wide-character window read returns a str (instr returns bytes).
312+ stdscr = self .stdscr
313+ s = 'a\u00e9 \u2502 z' # 'a', 'e'+acute (precomposed), box vline, 'z'
314+ stdscr .addstr (0 , 0 , s )
315+ self .assertEqual (stdscr .in_wstr (0 , 0 , len (s )), s )
316+ self .assertIsInstance (stdscr .instr (0 , 0 , len (s )), bytes )
317+
318+
256319 def test_output_character (self ):
257320 stdscr = self .stdscr
258321 encoding = stdscr .encoding
@@ -281,13 +344,16 @@ def test_output_character(self):
281344 stdscr .echochar ('A' )
282345 stdscr .echochar (b'A' )
283346 stdscr .echochar (65 )
284- with self .assertRaises ((UnicodeEncodeError , OverflowError )):
285- # Unicode is not fully supported yet, but at least it does
286- # not crash.
287- # It is supposed to fail because either the character is
288- # not encodable with the current encoding, or it is encoded to
289- # a multibyte sequence.
290- stdscr .echochar ('\u0114 ' )
347+ c = '\u0114 '
348+ try :
349+ stdscr .echochar (c )
350+ except UnicodeEncodeError :
351+ # The character is not encodable with the current encoding.
352+ self .assertRaises (UnicodeEncodeError , c .encode , encoding )
353+ except OverflowError :
354+ # The character is encoded to a multibyte sequence.
355+ encoded = c .encode (encoding )
356+ self .assertNotEqual (len (encoded ), 1 , repr (encoded ))
291357 stdscr .echochar ('A' , curses .A_BOLD )
292358 self .assertIs (stdscr .is_wintouched (), False )
293359
@@ -742,7 +808,6 @@ def test_borders_and_lines(self):
742808 self .assertEqual (win .inch (3 , 1 ), b'a' [0 ])
743809
744810 def test_unctrl (self ):
745- # TODO: wunctrl()
746811 self .assertEqual (curses .unctrl (b'A' ), b'A' )
747812 self .assertEqual (curses .unctrl ('A' ), b'A' )
748813 self .assertEqual (curses .unctrl (65 ), b'A' )
@@ -753,6 +818,21 @@ def test_unctrl(self):
753818 self .assertRaises (TypeError , curses .unctrl , b'AB' )
754819 self .assertRaises (TypeError , curses .unctrl , '' )
755820 self .assertRaises (TypeError , curses .unctrl , 'AB' )
821+
822+ @requires_curses_func ('wunctrl' )
823+ def test_wunctrl (self ):
824+ # The wide-character variant of unctrl() returns a str.
825+ self .assertEqual (curses .wunctrl (b'A' ), 'A' )
826+ self .assertEqual (curses .wunctrl ('A' ), 'A' )
827+ self .assertEqual (curses .wunctrl (65 ), 'A' )
828+ self .assertEqual (curses .wunctrl ('\n ' ), '^J' )
829+ self .assertEqual (curses .wunctrl (10 ), '^J' )
830+ self .assertEqual (curses .wunctrl ('é' ), 'é' ) # printable
831+ self .assertRaises (TypeError , curses .wunctrl , b'' )
832+ self .assertRaises (TypeError , curses .wunctrl , b'AB' )
833+ self .assertRaises (TypeError , curses .wunctrl , '' )
834+ # More than one spacing character is not a single cell.
835+ self .assertRaises (ValueError , curses .wunctrl , 'AB' )
756836 self .assertRaises (OverflowError , curses .unctrl , 2 ** 64 )
757837
758838 def test_endwin (self ):
@@ -800,7 +880,7 @@ def test_misc_module_funcs(self):
800880 curses .newpad (50 , 50 )
801881
802882 def test_env_queries (self ):
803- # TODO: term_attrs(), erasewchar(), killwchar()
883+ # TODO: term_attrs()
804884 self .assertIsInstance (curses .termname (), bytes )
805885 self .assertIsInstance (curses .longname (), bytes )
806886 self .assertIsInstance (curses .baudrate (), int )
@@ -815,6 +895,24 @@ def test_env_queries(self):
815895 self .assertIsInstance (c , bytes )
816896 self .assertEqual (len (c ), 1 )
817897
898+ # The erase and kill characters are a property of the controlling
899+ # terminal: the wide variants report ERR (raising curses.error) without
900+ # one, while the narrow variants above return an unspecified byte.
901+ try :
902+ tty_fd = os .open (os .ctermid (), os .O_RDONLY )
903+ except OSError :
904+ tty_fd = None
905+ if tty_fd is not None :
906+ os .close (tty_fd )
907+ if hasattr (curses , 'erasewchar' ):
908+ c = curses .erasewchar ()
909+ self .assertIsInstance (c , str )
910+ self .assertEqual (len (c ), 1 )
911+ if hasattr (curses , 'killwchar' ):
912+ c = curses .killwchar ()
913+ self .assertIsInstance (c , str )
914+ self .assertEqual (len (c ), 1 )
915+
818916 def test_output_options (self ):
819917 stdscr = self .stdscr
820918
0 commit comments