diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 60c60a9edeec49c..71eadfd5ca2ad70 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -841,6 +841,14 @@ The module :mod:`!curses` defines the following functions: appearance of the screen. +.. function:: term_attrs() + + Like :func:`termattrs`, but return the attributes as :ref:`WA_* + ` values rather than ``A_*`` values. + + .. versionadded:: next + + .. function:: termname() Return the value of the environment variable :envvar:`TERM`, as a bytes object, diff --git a/Doc/library/itertools.rst b/Doc/library/itertools.rst index 06f8bf2a8b6fa81..65442021ce1795d 100644 --- a/Doc/library/itertools.rst +++ b/Doc/library/itertools.rst @@ -197,6 +197,10 @@ loops that truncate the stream. for iterable in iterables: yield from iterable + Note that :pep:`798` unpacking syntax provides similar functionality + so that ``list(chain(p, q))`` could be written as + ``[*s for s in (p, q)]``. + .. classmethod:: chain.from_iterable(iterable) @@ -208,6 +212,10 @@ loops that truncate the stream. for iterable in iterables: yield from iterable + Note that :pep:`798` unpacking syntax provides similar functionality + so that ``list(chain.from_iterable(iterables))`` could be written as + ``[*s for s in iterables]``. + .. function:: combinations(iterable, r) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 7841fa56cc5952c..90ab3ee630b165e 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -165,6 +165,11 @@ curses :func:`~curses.scr_set`, which dump the whole screen to a file and restore it. (Contributed by Serhiy Storchaka in :gh:`152260`.) +* Add the :func:`curses.term_attrs` function, which returns the supported + video attributes as :ref:`WA_* ` values, the + counterpart of :func:`curses.termattrs`. + (Contributed by Serhiy Storchaka in :gh:`152332`.) + * Add the :mod:`curses` key-management functions :func:`~curses.define_key`, :func:`~curses.key_defined` and :func:`~curses.keyok`, available when built against an ncurses with ``NCURSES_EXT_FUNCS``. diff --git a/Lib/profiling/sampling/gecko_collector.py b/Lib/profiling/sampling/gecko_collector.py index 2bb5bd2f664d59f..2de8cce387e7f26 100644 --- a/Lib/profiling/sampling/gecko_collector.py +++ b/Lib/profiling/sampling/gecko_collector.py @@ -250,6 +250,25 @@ def collect(self, stack_frames, timestamps_us=None): self.interval = (times[-1] - self.last_sample_time) / self.sample_count self.last_sample_time = times[-1] + # Process async tasks + if stack_frames and hasattr(stack_frames[0], "awaited_by"): + for frames, thread_id, _ in self._iter_async_frames(stack_frames): + frames = filter_internal_frames(frames) + if not frames: + continue + + if thread_id not in self.threads: + self.threads[thread_id] = self._create_thread( + thread_id, False + ) + + self._record_stack_sample( + self.threads[thread_id], frames, thread_id, times, first_time + ) + + self.sample_count += len(times) + return + # Process threads for interpreter_info in stack_frames: for thread_info in interpreter_info.threads: @@ -333,37 +352,43 @@ def collect(self, stack_frames, timestamps_us=None): if not frames: continue - # Process stack once to get stack_index - stack_index = self._process_stack(thread_data, frames) - - # Add samples with timestamps - thread_spill = thread_data["_spill"] - for t in times: - thread_spill.append_sample(stack_index, t) - - # Handle opcodes - if self.opcodes_enabled and frames: - leaf_frame = frames[0] - filename, location, funcname, opcode = leaf_frame - if isinstance(location, tuple): - lineno, _, col_offset, _ = location - else: - lineno = location - col_offset = -1 - - current_state = (opcode, lineno, col_offset, funcname, filename) - - if tid not in self.opcode_state: - self.opcode_state[tid] = (*current_state, first_time) - elif self.opcode_state[tid][:5] != current_state: - prev_opcode, prev_lineno, prev_col, prev_funcname, prev_filename, prev_start = self.opcode_state[tid] - self._add_opcode_interval_marker( - tid, prev_opcode, prev_lineno, prev_col, prev_funcname, prev_start, first_time - ) - self.opcode_state[tid] = (*current_state, first_time) + self._record_stack_sample( + thread_data, frames, tid, times, first_time + ) self.sample_count += len(times) + def _record_stack_sample(self, thread_data, frames, tid, times, first_time): + stack_index = self._process_stack(thread_data, frames) + + thread_spill = thread_data["_spill"] + for t in times: + thread_spill.append_sample(stack_index, t) + + if self.opcodes_enabled and frames: + leaf_frame = frames[0] + filename, location, funcname, opcode = leaf_frame + if isinstance(location, tuple): + lineno, _, col_offset, _ = location + else: + lineno = location + col_offset = -1 + + current_state = (opcode, lineno, col_offset, funcname, filename) + + if tid not in self.opcode_state: + self.opcode_state[tid] = (*current_state, first_time) + elif self.opcode_state[tid][:5] != current_state: + ( + prev_opcode, prev_lineno, prev_col, prev_funcname, + prev_filename, prev_start + ) = self.opcode_state[tid] + self._add_opcode_interval_marker( + tid, prev_opcode, prev_lineno, prev_col, prev_funcname, + prev_start, first_time + ) + self.opcode_state[tid] = (*current_state, first_time) + def _create_thread(self, tid, is_main_thread): """Create a new thread structure with processed profile format.""" if self.spill_dir is None: diff --git a/Lib/test/test_curses.py b/Lib/test/test_curses.py index c17f4c87705c46c..5f01ac6546a0ba0 100644 --- a/Lib/test/test_curses.py +++ b/Lib/test/test_curses.py @@ -306,6 +306,14 @@ def test_refresh_control(self): 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. + def _encodable(self, s): # Wide characters are only supported in a locale that can encode them. try: @@ -314,6 +322,17 @@ def _encodable(self, s): return False return True + 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_curses_window_meth('get_wch') def test_addch_combining(self): stdscr = self.stdscr @@ -408,11 +427,19 @@ def test_complexchar_in_cell_methods(self): @requires_curses_window_meth('in_wstr') 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 - s = 'a\u00e9\u2502z' # 'a', 'e'+acute (precomposed), box vline, 'z' - stdscr.addstr(0, 0, s) - self.assertEqual(stdscr.in_wstr(0, 0, len(s)), s) - self.assertIsInstance(stdscr.instr(0, 0, len(s)), bytes) + 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._encodable(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) @requires_curses_func('complexchar') def test_complexchar(self): @@ -467,9 +494,12 @@ def test_in_wch(self): cc = stdscr.in_wch(0, 0) self.assertEqual(str(cc), 'A') self.assertTrue(cc.attr & curses.A_UNDERLINE) - if self._encodable('\u00e9'): # precomposed, for a portable round-trip - stdscr.addch(3, 0, curses.complexchar('\u00e9')) - self.assertEqual(str(stdscr.in_wch(3, 0)), '\u00e9') + # A character round-trips through the cell. See _encodable for the set. + for ch in ('A', '\u00e9', '\u00a4', '\u20ac', '\u0454'): + if self._encodable(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') @@ -498,6 +528,13 @@ def test_getbkgrnd(self): 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._encodable(ch): + with self.subTest(ch=ch): + stdscr.bkgd(curses.complexchar(ch)) + self.assertEqual(str(stdscr.getbkgrnd()), ch) + stdscr.bkgd(' ') @requires_curses_func('complexstr') def test_complexstr(self): @@ -648,35 +685,90 @@ def test_output_character(self): stdscr.addch('A') stdscr.addch(b'A') stdscr.addch(65) - c = '\u20ac' - 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)) + # 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() round-trips a byte only where its code point equals + # the byte value (Latin-1): on a wide build ncurses winsch + # stores a printable byte directly as a code point instead of + # decoding it through the locale. + if ord(c) < 0x100: + 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. + wide = hasattr(stdscr, 'in_wch') + for c in ('é', '¤', '€', 'є'): + if not self._encodable(c): + continue + if not wide and len(c.encode(encoding)) != 1: + 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) - c = '\u0114' - 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)) + # 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) @@ -686,14 +778,18 @@ def test_output_string(self): # addstr()/insstr() for func in [stdscr.addstr, stdscr.insstr]: with self.subTest(func.__qualname__): - stdscr.move(0, 0) func('abcd') func(b'abcd') - s = 'àßçđ' - try: - func(s) - except UnicodeEncodeError: - self.assertRaises(UnicodeEncodeError, s.encode, encoding) + # 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) @@ -704,11 +800,14 @@ def test_output_string(self): stdscr.move(0, 0) func('1234', 3) func(b'1234', 3) - s = '\u0661\u0662\u0663\u0664' - try: - func(s, 3) - except UnicodeEncodeError: - self.assertRaises(UnicodeEncodeError, s.encode, encoding) + # 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) @@ -798,6 +897,24 @@ def test_read_from_window(self): 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). instr() returns the locale bytes + # for any single-byte character; inch() packs the text into a chtype, so + # on a wide build it only round-trips a Latin-1 codepoint (byte == + # codepoint). + 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) + if ord(ch) < 0x100: + 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. @@ -839,6 +956,10 @@ def test_getch(self): self.assertEqual(win.getch(), b'm'[0]) self.assertEqual(win.getch(), b'\n'[0]) + # A key value > 127 is delivered unchanged (it is not locale text). + curses.ungetch(0xE9) + self.assertEqual(win.getch(), 0xE9) + def test_getstr(self): win = curses.newwin(5, 12, 5, 2) curses.echo() @@ -861,6 +982,25 @@ def test_getstr(self): self.assertEqual(win.getstr(), b'amet') self.assertEqual(win.instr(1, 0), b'amet dolor ') + @requires_curses_window_meth('get_wstr') + 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._encodable(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) @@ -1059,6 +1199,29 @@ def test_background(self): 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 of an 8-bit locale reads back as its + # encoded byte. See _encodable for the character set. + win.bkgd(' ') + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + win.bkgd(ch) + self.assertEqual(win.getbkgd(), b[0]) + 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(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + win.bkgd(b[0]) + self.assertEqual(win.getbkgd(), b[0]) + def test_overlay(self): srcwin = curses.newwin(5, 18, 3, 4) lorem_ipsum(srcwin) @@ -1182,6 +1345,16 @@ def test_borders_and_lines(self): 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'~~~~~~~~') @@ -1192,6 +1365,11 @@ def test_borders_and_lines(self): 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) @@ -1213,6 +1391,39 @@ def test_borders_and_lines(self): 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 of an 8-bit locale round-trips as its + # encoded byte. See _encodable for the character set. + encoding = win.encoding + for ch in ('é', '¤', '€', 'є'): + try: + b = ch.encode(encoding) + except UnicodeEncodeError: + continue + if len(b) != 1: + continue + with self.subTest(ch=ch): + win.erase() + win.hline(2, 0, ch, 5) + self.assertEqual(win.instr(2, 0, 5), b * 5) + 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. + v = b[0] + 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') @@ -1220,6 +1431,19 @@ def test_unctrl(self): 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, '') @@ -1233,7 +1457,9 @@ def test_wunctrl(self): self.assertEqual(curses.wunctrl(65), 'A') self.assertEqual(curses.wunctrl('\n'), '^J') self.assertEqual(curses.wunctrl(10), '^J') - self.assertEqual(curses.wunctrl('é'), 'é') # printable + # See _encodable for the character set (all printable here). + for c in ('A', 'é', '¤', '€', 'є'): + self.assertEqual(curses.wunctrl(c), c) self.assertRaises(TypeError, curses.wunctrl, b'') self.assertRaises(TypeError, curses.wunctrl, b'AB') self.assertRaises(TypeError, curses.wunctrl, '') @@ -1286,13 +1512,13 @@ def test_misc_module_funcs(self): curses.newpad(50, 50) def test_env_queries(self): - # TODO: term_attrs() 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) + self.assertIsInstance(curses.term_attrs(), int) c = curses.killchar() self.assertIsInstance(c, bytes) @@ -2136,7 +2362,8 @@ def test_issue6243(self): def test_unget_wch(self): stdscr = self.stdscr encoding = stdscr.encoding - for ch in ('a', '\xe9', '\u20ac', '\U0010FFFF'): + # See _encodable for the character set, plus a non-BMP character. + for ch in ('a', '\xe9', '\xa4', '\u20ac', '\u0454', '\U0010FFFF'): try: ch.encode(encoding) except UnicodeEncodeError: diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py index 56f3fe5e1c2605c..d440c44385e6713 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_collectors.py @@ -40,7 +40,16 @@ from test.support import captured_stdout, captured_stderr -from .mocks import MockFrameInfo, MockThreadInfo, MockInterpreterInfo, LocationInfo, make_diff_collector_with_mock_baseline +from .mocks import ( + MockAwaitedInfo, + MockCoroInfo, + MockFrameInfo, + MockInterpreterInfo, + MockTaskInfo, + MockThreadInfo, + LocationInfo, + make_diff_collector_with_mock_baseline, +) from .helpers import close_and_unlink, jsonl_tables @@ -673,6 +682,48 @@ def test_gecko_collector_basic(self): self.assertGreater(stack_table["length"], 0) self.assertGreater(len(stack_table["frame"]), 0) + def test_gecko_collector_async_aware(self): + collector = GeckoCollector(1000) + + parent = MockTaskInfo( + task_id=1, + task_name="Parent", + coroutine_stack=[ + MockCoroInfo( + task_name="Parent", + call_stack=[MockFrameInfo("parent.py", 10, "parent_fn")], + ) + ], + ) + child = MockTaskInfo( + task_id=2, + task_name="Child", + coroutine_stack=[ + MockCoroInfo( + task_name="Child", + call_stack=[MockFrameInfo("child.py", 20, "child_fn")], + ) + ], + awaited_by=[MockCoroInfo(task_name=1, call_stack=[])], + ) + + collector.collect( + [MockAwaitedInfo(thread_id=100, awaited_by=[parent, child])], + timestamps_us=[1000, 2000], + ) + profile_data = export_gecko_profile(self, collector) + + self.assertEqual(len(profile_data["threads"]), 1) + thread_data = profile_data["threads"][0] + self.assertEqual(thread_data["samples"]["length"], 2) + + string_array = profile_data["shared"]["stringArray"] + self.assertIn("parent_fn", string_array) + self.assertIn("child_fn", string_array) + self.assertIn("Parent", string_array) + self.assertIn("Child", string_array) + self.assertEqual(thread_data["markers"]["length"], 0) + @unittest.skipIf(is_emscripten, "threads not available") def test_gecko_collector_export(self): """Test Gecko profile export functionality.""" diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 4c003e697d23b86..a93f5dee349e648 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -2,6 +2,7 @@ import functools import platform import sys +import textwrap import unittest import weakref import tkinter @@ -9,6 +10,7 @@ import enum from test import support from test.support import os_helper +from test.support.script_helper import assert_python_ok from test.test_tkinter.support import setUpModule # noqa: F401 from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest, requires_tk, get_tk_patchlevel, @@ -53,6 +55,33 @@ class Button2(tkinter.Button): b4 = Button2(f2) self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4) + def test_dealloc_in_wrong_thread(self): + # gh-83274: deallocating the interpreter in the wrong thread must not + # crash. + script = textwrap.dedent(""" + import threading + import tkinter + root = tkinter.Tk() + root.destroy() + # Let another thread drop the last reference. + ready = threading.Event() + t = threading.Thread(target=lambda obj: ready.wait(), args=(root,)) + t.start() + del root + ready.set() + t.join() + print('ok') + """) + rc, out, err = assert_python_ok('-c', script) + self.assertEqual(out.strip(), b'ok') + if not support.Py_GIL_DISABLED: + # On the free-threaded build the interpreter may instead be + # deallocated in its own thread (deferred reference counting), so + # the warning is not necessarily emitted. The crucial guarantee -- + # no crash -- is already checked by assert_python_ok() above. + self.assertIn(b'RuntimeWarning', err) + self.assertIn(b'gh-83274', err) + @requires_tk(8, 6, 6) def test_tk_busy(self): root = self.root diff --git a/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst b/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst new file mode 100644 index 000000000000000..994e5dadcf2e8a0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-25-20-00-00.gh-issue-151496.kMnT3x.rst @@ -0,0 +1,3 @@ +Fixed ``profiling.sampling --gecko`` with ``--async-aware`` by flattening +async task stacks before generating Gecko samples. ``--binary`` now rejects +``--async-aware`` until the binary format supports async task data. diff --git a/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst new file mode 100644 index 000000000000000..3b722d2176be920 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-16-30-00.gh-issue-83274.Kx9mQv.rst @@ -0,0 +1,3 @@ +Deallocating a :mod:`tkinter` application from a thread other than the one it +was created in no longer crashes the interpreter. The underlying Tcl +interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported. diff --git a/Misc/NEWS.d/next/Library/2026-06-26-22-15-00.gh-issue-152332.Lp3WqN.rst b/Misc/NEWS.d/next/Library/2026-06-26-22-15-00.gh-issue-152332.Lp3WqN.rst new file mode 100644 index 000000000000000..32875cdac5c9d0c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-22-15-00.gh-issue-152332.Lp3WqN.rst @@ -0,0 +1,2 @@ +Add the :func:`curses.term_attrs` function, the counterpart of +:func:`curses.termattrs` for the ``WA_*`` attributes. diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 76e93784b94d466..5e50f9b1e0b2384 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -7347,6 +7347,24 @@ _curses_termattrs_impl(PyObject *module) /*[clinic end generated code: output=b06f437fce1b6fc4 input=0559882a04f84d1d]*/ NoArgReturnIntFunctionBody(termattrs) +/*[clinic input] +_curses.term_attrs + +Return a logical OR of all video attributes supported by the terminal. + +The attributes are WA_* values, the extended-attribute counterparts of +the A_* values returned by termattrs(). +[clinic start generated code]*/ + +static PyObject * +_curses_term_attrs_impl(PyObject *module) +/*[clinic end generated code: output=c559daa1370948d6 input=963136fd17ab797a]*/ +{ + PyCursesStatefulInitialised(module); + + return PyLong_FromUnsignedLong(term_attrs()); +} + /*[clinic input] @permit_long_summary _curses.termname @@ -7905,6 +7923,7 @@ static PyMethodDef cursesmodule_methods[] = { _CURSES_SETUPTERM_METHODDEF _CURSES_START_COLOR_METHODDEF _CURSES_TERMATTRS_METHODDEF + _CURSES_TERM_ATTRS_METHODDEF _CURSES_TERMNAME_METHODDEF _CURSES_TIGETFLAG_METHODDEF _CURSES_TIGETNUM_METHODDEF diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 8fa58d07096e30b..1f1fe4a91addf4c 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op) { TkappObject *self = TkappObject_CAST(op); PyTypeObject *tp = Py_TYPE(self); - /*CHECK_TCL_APPARTMENT;*/ - ENTER_TCL - Tcl_DeleteInterp(Tkapp_Interp(self)); - LEAVE_TCL + if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + /* Deleting the interpreter from another thread aborts the process + ("Tcl_AsyncDelete: async handler deleted by the wrong thread"). + Leak it instead (gh-83274). */ + if (PyErr_WarnEx(PyExc_RuntimeWarning, + "the Tcl interpreter is leaked because it was " + "deallocated in a thread other than the one it was " + "created in (see gh-83274)", 1) < 0) + { + PyErr_FormatUnraisable("Exception ignored while finalizing " + "a Tcl interpreter"); + } + } + else { + ENTER_TCL + Tcl_DeleteInterp(Tkapp_Interp(self)); + LEAVE_TCL + } Py_XDECREF(self->trace); PyObject_Free(self); Py_DECREF(tp); diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index a4a7791fa6adcd6..1c05a125fd1cc6c 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -5073,6 +5073,27 @@ _curses_termattrs(PyObject *module, PyObject *Py_UNUSED(ignored)) return _curses_termattrs_impl(module); } +PyDoc_STRVAR(_curses_term_attrs__doc__, +"term_attrs($module, /)\n" +"--\n" +"\n" +"Return a logical OR of all video attributes supported by the terminal.\n" +"\n" +"The attributes are WA_* values, the extended-attribute counterparts of\n" +"the A_* values returned by termattrs()."); + +#define _CURSES_TERM_ATTRS_METHODDEF \ + {"term_attrs", (PyCFunction)_curses_term_attrs, METH_NOARGS, _curses_term_attrs__doc__}, + +static PyObject * +_curses_term_attrs_impl(PyObject *module); + +static PyObject * +_curses_term_attrs(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _curses_term_attrs_impl(module); +} + PyDoc_STRVAR(_curses_termname__doc__, "termname($module, /)\n" "--\n" @@ -5645,4 +5666,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=01cb1ecb396881c9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=976a6629bfe58a3d input=a9049054013a1b77]*/