@@ -469,6 +469,145 @@ def test_getbkgrnd(self):
469469 self .assertEqual (str (cc ), ' ' )
470470 self .assertTrue (cc .attr & curses .A_BOLD )
471471
472+ @requires_curses_func ('complexstr' )
473+ def test_complexstr (self ):
474+ # A complexstr is an immutable run of styled wide-character cells: the
475+ # string counterpart of complexchar (as str is to a single character).
476+ cc = curses .complexchar
477+ B = curses .A_BOLD
478+ # Built from an iterable whose items are complexchar or str cells.
479+ s = curses .complexstr ([cc ('A' , B ), 'b' , cc ('c' )])
480+ self .assertEqual (len (s ), 3 )
481+ self .assertEqual (str (s ), 'Abc' )
482+ # Indexing yields a complexchar carrying the cell's rendition.
483+ self .assertIsInstance (s [0 ], curses .complexchar )
484+ self .assertEqual (str (s [0 ]), 'A' )
485+ self .assertTrue (s [0 ].attr & B )
486+ self .assertEqual (s [- 1 ], cc ('c' ))
487+ self .assertRaises (IndexError , lambda : s [3 ])
488+ # Iteration walks the cells.
489+ self .assertEqual ([str (c ) for c in s ], ['A' , 'b' , 'c' ])
490+ # Slicing and concatenation produce new complexstr instances.
491+ self .assertIsInstance (s [1 :], curses .complexstr )
492+ self .assertEqual (str (s [1 :]), 'bc' )
493+ self .assertEqual (str (s [::- 1 ]), 'cbA' )
494+ self .assertEqual (str (s + curses .complexstr (['Z' ])), 'AbcZ' )
495+ # The empty complexstr.
496+ self .assertEqual (len (curses .complexstr ([])), 0 )
497+ self .assertEqual (str (curses .complexstr ('' )), '' )
498+ # Equality and hashing compare the cells (text, attributes, pair).
499+ self .assertEqual (s , curses .complexstr ([cc ('A' , B ), 'b' , cc ('c' )]))
500+ self .assertEqual (hash (s ),
501+ hash (curses .complexstr ([cc ('A' , B ), 'b' , cc ('c' )])))
502+ self .assertNotEqual (s , curses .complexstr ([cc ('A' ), 'b' , cc ('c' )]))
503+ self .assertNotEqual (s , curses .complexstr ([cc ('A' , B ), 'b' ]))
504+ # A spacing character optionally followed by combining characters.
505+ if self ._encodable ('é' ):
506+ self .assertEqual (str (curses .complexstr (['é' , 'x' ])),
507+ 'éx' )
508+ # cells is positional-only.
509+ self .assertRaises (TypeError , lambda : curses .complexstr (cells = ['x' ]))
510+ # Invalid arguments.
511+ self .assertRaises (TypeError , curses .complexstr , 5 )
512+ self .assertRaises (TypeError , curses .complexstr , [65 ])
513+ self .assertRaises (ValueError , curses .complexstr , ['ab' ])
514+
515+ # A string is split into character cells, grouping each base character
516+ # with the combining characters that follow it (not one cell per code
517+ # point), unlike a generic sequence whose items are each one cell.
518+ self .assertEqual (len (curses .complexstr ('abc' )), 3 )
519+ self .assertEqual (str (curses .complexstr ('abc' )), 'abc' )
520+ self .assertEqual (len (curses .complexstr ('' )), 0 )
521+ base = 'é' # 'e' + combining acute: two code points, one cell
522+ if self ._encodable (base ):
523+ self .assertEqual (len (curses .complexstr (base )), 1 )
524+ self .assertEqual (curses .complexstr (base )[0 ], cc (base ))
525+ self .assertEqual (len (curses .complexstr ('a' + base + 'b' )), 3 )
526+ # A combining character cannot begin a cell: one that leads the
527+ # string, or overflows a base's combining slots, has no base.
528+ self .assertRaises (ValueError , curses .complexstr , '\u0301 ' )
529+ self .assertRaises (ValueError , curses .complexstr , 'e' + '\u0301 ' * 10 )
530+ # A control character may stand alone but not carry combining marks.
531+ self .assertRaises (ValueError , curses .complexstr , '\n \u0301 ' )
532+ # attr and pair apply to every cell of a string; pair is optional.
533+ styled = curses .complexstr ('hi' , B , 0 )
534+ self .assertTrue (all (styled [i ].attr & B for i in range (len (styled ))))
535+ self .assertEqual (curses .complexstr ('x' , B )[0 ], cc ('x' , B ))
536+ self .assertEqual (curses .complexstr ('x' , B , 0 )[0 ], cc ('x' , B , 0 ))
537+ # attr and pair may also be passed by keyword.
538+ self .assertEqual (curses .complexstr ('x' , attr = B )[0 ], cc ('x' , B ))
539+ self .assertEqual (curses .complexstr ('x' , attr = B , pair = 0 )[0 ], cc ('x' , B , 0 ))
540+ self .assertEqual (curses .complexstr ('x' , pair = 0 )[0 ], cc ('x' , 0 , 0 ))
541+ # cells is positional-only.
542+ self .assertRaises (TypeError , lambda : curses .complexstr (cells = 'x' ))
543+ self .assertRaises (ValueError , curses .complexstr , 'a' , 0 , - 1 )
544+ self .assertRaises (ValueError , lambda : curses .complexstr ('a' , pair = - 1 ))
545+ # For a non-string, giving attr/pair at all is an error (the cells
546+ # carry their own rendition) -- even attr=0.
547+ self .assertRaises (TypeError , curses .complexstr , [cc ('A' )], B )
548+ self .assertRaises (TypeError , curses .complexstr , [cc ('A' )], 0 )
549+ self .assertRaises (TypeError , curses .complexstr , ['A' ], 0 , 0 )
550+ self .assertRaises (TypeError ,
551+ lambda : curses .complexstr ([cc ('A' )], attr = B ))
552+ self .assertRaises (TypeError ,
553+ lambda : curses .complexstr (['A' ], pair = 0 ))
554+
555+ @requires_curses_window_meth ('in_wchstr' )
556+ def test_in_wchstr (self ):
557+ # in_wchstr() returns a complexstr -- the styled-cell counterpart of
558+ # instr() (bytes) and in_wstr() (str), which both strip the rendition.
559+ stdscr = self .stdscr
560+ cc = curses .complexchar
561+ B = curses .A_BOLD
562+ s = curses .complexstr ([cc ('A' , B ), cc ('b' ), cc ('C' , B )])
563+ stdscr .addstr (0 , 0 , s )
564+ r = stdscr .in_wchstr (0 , 0 , 3 )
565+ self .assertIsInstance (r , curses .complexstr )
566+ # A read followed by a re-write is an exact round-trip.
567+ self .assertEqual (r , s )
568+ self .assertEqual (str (r ), 'AbC' )
569+ self .assertTrue (r [0 ].attr & B )
570+ self .assertFalse (r [1 ].attr & B )
571+ # The count is optional and reads to the end of the line by default.
572+ stdscr .move (0 , 0 )
573+ self .assertEqual (str (stdscr .in_wchstr ())[:3 ], 'AbC' )
574+
575+ @requires_curses_window_meth ('in_wchstr' )
576+ def test_complexstr_in_write_methods (self ):
577+ # addstr/addnstr/insstr/insnstr also accept a complexstr, written via
578+ # the wide-character functions; a plain str keeps its current meaning.
579+ stdscr = self .stdscr
580+ cc = curses .complexchar
581+ B = curses .A_BOLD
582+ s = curses .complexstr ([cc ('A' , B ), cc ('b' ), cc ('C' , B )])
583+ # addstr with a complexstr round-trips.
584+ stdscr .addstr (0 , 0 , s )
585+ self .assertEqual (stdscr .in_wchstr (0 , 0 , 3 ), s )
586+ # addnstr writes at most n cells.
587+ stdscr .addstr (2 , 0 , '....' )
588+ stdscr .addnstr (2 , 0 , s , 2 )
589+ self .assertEqual (str (stdscr .in_wchstr (2 , 0 , 4 )), 'Ab..' )
590+ # insstr inserts the cells in order.
591+ stdscr .move (3 , 0 )
592+ stdscr .addstr ('END' )
593+ stdscr .insstr (3 , 0 , curses .complexstr ([cc ('P' ), cc ('Q' )]))
594+ self .assertEqual (str (stdscr .in_wchstr (3 , 0 , 5 )), 'PQEND' )
595+ # insnstr inserts at most n cells.
596+ stdscr .move (4 , 0 )
597+ stdscr .addstr ('END' )
598+ stdscr .insnstr (4 , 0 , curses .complexstr (['1' , '2' , '3' ]), 2 )
599+ self .assertEqual (str (stdscr .in_wchstr (4 , 0 , 5 )), '12END' )
600+ # An empty run is accepted (and still honours the move).
601+ stdscr .addstr (5 , 0 , curses .complexstr ([]))
602+ stdscr .insstr (5 , 0 , curses .complexstr ([]))
603+ # Cells carry their own rendition, so an explicit attr is rejected.
604+ self .assertRaises (TypeError , stdscr .addstr , s , B )
605+ self .assertRaises (TypeError , stdscr .addnstr , s , 2 , B )
606+ self .assertRaises (TypeError , stdscr .insstr , s , B )
607+ self .assertRaises (TypeError , stdscr .insnstr , s , 2 , B )
608+ # A bare sequence of cells is not accepted; build a complexstr first.
609+ self .assertRaises (TypeError , stdscr .addstr , [cc ('A' ), 'b' ])
610+ self .assertRaises (TypeError , stdscr .insstr , [cc ('A' ), 'b' ])
472611
473612 def test_output_character (self ):
474613 stdscr = self .stdscr
0 commit comments