diff --git a/Lib/_colorize.py b/Lib/_colorize.py index 41e818f2a747ff..9eb6f0933b8150 100644 --- a/Lib/_colorize.py +++ b/Lib/_colorize.py @@ -1,23 +1,64 @@ +from __future__ import annotations import io import os import sys COLORIZE = True +# types +if False: + from typing import IO + class ANSIColors: - BACKGROUND_YELLOW = "\x1b[43m" - BOLD_GREEN = "\x1b[1;32m" - BOLD_MAGENTA = "\x1b[1;35m" - BOLD_RED = "\x1b[1;31m" + RESET = "\x1b[0m" + BLACK = "\x1b[30m" + BLUE = "\x1b[34m" + CYAN = "\x1b[36m" GREEN = "\x1b[32m" - GREY = "\x1b[90m" MAGENTA = "\x1b[35m" RED = "\x1b[31m" - RESET = "\x1b[0m" + WHITE = "\x1b[37m" # more like LIGHT GRAY YELLOW = "\x1b[33m" + BOLD_BLACK = "\x1b[1;30m" # DARK GRAY + BOLD_BLUE = "\x1b[1;34m" + BOLD_CYAN = "\x1b[1;36m" + BOLD_GREEN = "\x1b[1;32m" + BOLD_MAGENTA = "\x1b[1;35m" + BOLD_RED = "\x1b[1;31m" + BOLD_WHITE = "\x1b[1;37m" # actual WHITE + BOLD_YELLOW = "\x1b[1;33m" + + # intense = like bold but without being bold + INTENSE_BLACK = "\x1b[90m" + INTENSE_BLUE = "\x1b[94m" + INTENSE_CYAN = "\x1b[96m" + INTENSE_GREEN = "\x1b[92m" + INTENSE_MAGENTA = "\x1b[95m" + INTENSE_RED = "\x1b[91m" + INTENSE_WHITE = "\x1b[97m" + INTENSE_YELLOW = "\x1b[93m" + + BACKGROUND_BLACK = "\x1b[40m" + BACKGROUND_BLUE = "\x1b[44m" + BACKGROUND_CYAN = "\x1b[46m" + BACKGROUND_GREEN = "\x1b[42m" + BACKGROUND_MAGENTA = "\x1b[45m" + BACKGROUND_RED = "\x1b[41m" + BACKGROUND_WHITE = "\x1b[47m" + BACKGROUND_YELLOW = "\x1b[43m" + + INTENSE_BACKGROUND_BLACK = "\x1b[100m" + INTENSE_BACKGROUND_BLUE = "\x1b[104m" + INTENSE_BACKGROUND_CYAN = "\x1b[106m" + INTENSE_BACKGROUND_GREEN = "\x1b[102m" + INTENSE_BACKGROUND_MAGENTA = "\x1b[105m" + INTENSE_BACKGROUND_RED = "\x1b[101m" + INTENSE_BACKGROUND_WHITE = "\x1b[107m" + INTENSE_BACKGROUND_YELLOW = "\x1b[103m" + NoColors = ANSIColors() @@ -26,14 +67,16 @@ class ANSIColors: setattr(NoColors, attr, "") -def get_colors(colorize: bool = False, *, file=None) -> ANSIColors: +def get_colors( + colorize: bool = False, *, file: IO[str] | IO[bytes] | None = None +) -> ANSIColors: if colorize or can_colorize(file=file): return ANSIColors() else: return NoColors -def can_colorize(*, file=None) -> bool: +def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool: if file is None: file = sys.stdout @@ -66,4 +109,4 @@ def can_colorize(*, file=None) -> bool: try: return os.isatty(file.fileno()) except io.UnsupportedOperation: - return file.isatty() + return hasattr(file, "isatty") and file.isatty() diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 503ca1da329eaa..cbb6d85f683257 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -456,7 +456,7 @@ def do(self) -> None: class show_history(Command): def do(self) -> None: from .pager import get_pager - from site import gethistoryfile # type: ignore[attr-defined] + from site import gethistoryfile history = os.linesep.join(self.reader.history[:]) self.reader.console.restore() diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 7916638921bbf2..8956fb1242e52a 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -19,7 +19,7 @@ from __future__ import annotations -import _colorize # type: ignore[import-not-found] +import _colorize from abc import ABC, abstractmethod import ast @@ -162,7 +162,7 @@ def __init__( *, local_exit: bool = False, ) -> None: - super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] + super().__init__(locals=locals, filename=filename, local_exit=local_exit) self.can_colorize = _colorize.can_colorize() def showsyntaxerror(self, filename=None, **kwargs): diff --git a/Lib/_pyrepl/mypy.ini b/Lib/_pyrepl/mypy.ini index 395f5945ab740b..eabd0e9b440bf4 100644 --- a/Lib/_pyrepl/mypy.ini +++ b/Lib/_pyrepl/mypy.ini @@ -4,8 +4,9 @@ [mypy] files = Lib/_pyrepl +mypy_path = $MYPY_CONFIG_FILE_DIR/../../Misc/mypy explicit_package_bases = True -python_version = 3.12 +python_version = 3.13 platform = linux pretty = True @@ -22,3 +23,7 @@ check_untyped_defs = False # Various internal modules that typeshed deliberately doesn't have stubs for: [mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*] ignore_missing_imports = True + +# Other untyped parts of the stdlib +[mypy-idlelib.*] +ignore_missing_imports = True diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 4795c51296a500..b38f0bf82db331 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -26,11 +26,11 @@ from contextlib import contextmanager from dataclasses import dataclass, field, fields import unicodedata -from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found] +from _colorize import can_colorize, ANSIColors from . import commands, console, input -from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width +from .utils import wlen, unbracket, str_width from .trace import trace @@ -421,42 +421,15 @@ def calc_screen(self) -> list[str]: @staticmethod def process_prompt(prompt: str) -> tuple[str, int]: - """Process the prompt. + r"""Return a tuple with the prompt string and its visible length. - This means calculate the length of the prompt. The character \x01 - and \x02 are used to bracket ANSI control sequences and need to be - excluded from the length calculation. So also a copy of the prompt - is returned with these control characters removed.""" - - # The logic below also ignores the length of common escape - # sequences if they were not explicitly within \x01...\x02. - # They are CSI (or ANSI) sequences ( ESC [ ... LETTER ) - - # wlen from utils already excludes ANSI_ESCAPE_SEQUENCE chars, - # which breaks the logic below so we redefine it here. - def wlen(s: str) -> int: - return sum(str_width(i) for i in s) - - out_prompt = "" - l = wlen(prompt) - pos = 0 - while True: - s = prompt.find("\x01", pos) - if s == -1: - break - e = prompt.find("\x02", s) - if e == -1: - break - # Found start and end brackets, subtract from string length - l = l - (e - s + 1) - keep = prompt[pos:s] - l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep))) - out_prompt += keep + prompt[s + 1 : e] - pos = e + 1 - keep = prompt[pos:] - l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep))) - out_prompt += keep - return out_prompt, l + The prompt string has the zero-width brackets recognized by shells + (\x01 and \x02) removed. The length ignores anything between those + brackets as well as any ANSI escape sequences. + """ + out_prompt = unbracket(prompt, including_content=False) + visible_prompt = unbracket(prompt, including_content=True) + return out_prompt, wlen(visible_prompt) def bow(self, p: int | None = None) -> int: """Return the 0-based index of the word break preceding p most diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 888185eb03be66..be229488e54e37 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -32,7 +32,7 @@ from dataclasses import dataclass, field import os -from site import gethistoryfile # type: ignore[attr-defined] +from site import gethistoryfile import sys from rlcompleter import Completer as RLCompleter diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 4651717bd7e121..0eb5f8c0097f41 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -3,6 +3,8 @@ import functools ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") +ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) @functools.cache @@ -10,16 +12,27 @@ def str_width(c: str) -> int: if ord(c) < 128: return 1 w = unicodedata.east_asian_width(c) - if w in ('N', 'Na', 'H', 'A'): + if w in ("N", "Na", "H", "A"): return 1 return 2 def wlen(s: str) -> int: - if len(s) == 1 and s != '\x1a': + if len(s) == 1 and s != "\x1a": return str_width(s) length = sum(str_width(i) for i in s) # remove lengths of any escape sequences sequence = ANSI_ESCAPE_SEQUENCE.findall(s) - ctrl_z_cnt = s.count('\x1a') + ctrl_z_cnt = s.count("\x1a") return length - sum(len(i) for i in sequence) + ctrl_z_cnt + + +def unbracket(s: str, including_content: bool = False) -> str: + r"""Return `s` with \001 and \002 characters removed. + + If `including_content` is True, content between \001 and \002 is also + stripped. + """ + if including_content: + return ZERO_WIDTH_BRACKET.sub("", s) + return s.translate(ZERO_WIDTH_TRANS) diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index db7c68681f62c4..3692e164cb9254 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -6,14 +6,24 @@ from _pyrepl.console import Console, Event from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.simple_interact import _strip_final_indent +from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE + + +class ScreenEqualMixin: + def assert_screen_equal( + self, reader: ReadlineAlikeReader, expected: str, clean: bool = False + ): + actual = clean_screen(reader) if clean else reader.screen + expected = expected.split("\n") + self.assertListEqual(actual, expected) def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None): saved = reader.more_lines try: reader.more_lines = partial(more_lines, namespace=namespace) - reader.ps1 = reader.ps2 = ">>>" - reader.ps3 = reader.ps4 = "..." + reader.ps1 = reader.ps2 = ">>> " + reader.ps3 = reader.ps4 = "... " return reader.readline() finally: reader.more_lines = saved @@ -38,18 +48,22 @@ def code_to_events(code: str): yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8"))) -def clean_screen(screen: Iterable[str]): +def clean_screen(reader: ReadlineAlikeReader) -> list[str]: """Cleans color and console characters out of a screen output. This is useful for screen testing, it increases the test readability since it strips out all the unreadable side of the screen. """ output = [] - for line in screen: - if line.startswith(">>>") or line.startswith("..."): - line = line[3:] + for line in reader.screen: + line = unbracket(line, including_content=True) + line = ANSI_ESCAPE_SEQUENCE.sub("", line) + for prefix in (reader.ps1, reader.ps2, reader.ps3, reader.ps4): + if line.startswith(prefix): + line = line[len(prefix):] + break output.append(line) - return "\n".join(output).strip() + return output def prepare_reader(console: Console, **kwargs): @@ -99,6 +113,9 @@ def handle_all_events( prepare_console=partial(prepare_console, width=10), ) +reader_no_colors = partial(prepare_reader, can_colorize=False) +reader_force_colors = partial(prepare_reader, can_colorize=True) + class FakeConsole(Console): def __init__(self, events, encoding="utf-8") -> None: diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index a6a66162d6b25a..8b063a25913b0b 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -17,12 +17,12 @@ from .support import ( FakeConsole, + ScreenEqualMixin, handle_all_events, handle_events_narrow_console, more_lines, multiline_input, code_to_events, - clean_screen, ) from _pyrepl.console import Event from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig, @@ -587,7 +587,7 @@ def test_auto_indent_ignore_comments(self): self.assertEqual(output, output_code) -class TestPyReplOutput(TestCase): +class TestPyReplOutput(ScreenEqualMixin, TestCase): def prepare_reader(self, events): console = FakeConsole(events) config = ReadlineConfig(readline_completer=None) @@ -620,7 +620,7 @@ def test_basic(self): output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) def test_get_line_buffer_returns_str(self): reader = self.prepare_reader(code_to_events("\n")) @@ -654,11 +654,13 @@ def test_multiline_edit(self): reader = self.prepare_reader(events) output = multiline_input(reader) - self.assertEqual(output, "def f():\n ...\n ") - self.assertEqual(clean_screen(reader.screen), "def f():\n ...") + expected = "def f():\n ...\n " + self.assertEqual(output, expected) + self.assert_screen_equal(reader, expected, clean=True) output = multiline_input(reader) - self.assertEqual(output, "def g():\n pass\n ") - self.assertEqual(clean_screen(reader.screen), "def g():\n pass") + expected = "def g():\n pass\n " + self.assertEqual(output, expected) + self.assert_screen_equal(reader, expected, clean=True) def test_history_navigation_with_up_arrow(self): events = itertools.chain( @@ -677,16 +679,16 @@ def test_history_navigation_with_up_arrow(self): output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) output = multiline_input(reader) self.assertEqual(output, "2+2") - self.assertEqual(clean_screen(reader.screen), "2+2") + self.assert_screen_equal(reader, "2+2", clean=True) output = multiline_input(reader) self.assertEqual(output, "2+2") - self.assertEqual(clean_screen(reader.screen), "2+2") + self.assert_screen_equal(reader, "2+2", clean=True) output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) def test_history_with_multiline_entries(self): code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n" @@ -705,11 +707,9 @@ def test_history_with_multiline_entries(self): output = multiline_input(reader) output = multiline_input(reader) output = multiline_input(reader) - self.assertEqual( - clean_screen(reader.screen), - 'def foo():\n x = 1\n y = 2\n z = 3' - ) - self.assertEqual(output, "def foo():\n x = 1\n y = 2\n z = 3\n ") + expected = "def foo():\n x = 1\n y = 2\n z = 3\n " + self.assert_screen_equal(reader, expected, clean=True) + self.assertEqual(output, expected) def test_history_navigation_with_down_arrow(self): @@ -728,7 +728,7 @@ def test_history_navigation_with_down_arrow(self): output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) def test_history_search(self): events = itertools.chain( @@ -745,23 +745,23 @@ def test_history_search(self): output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) output = multiline_input(reader) self.assertEqual(output, "2+2") - self.assertEqual(clean_screen(reader.screen), "2+2") + self.assert_screen_equal(reader, "2+2", clean=True) output = multiline_input(reader) self.assertEqual(output, "3+3") - self.assertEqual(clean_screen(reader.screen), "3+3") + self.assert_screen_equal(reader, "3+3", clean=True) output = multiline_input(reader) self.assertEqual(output, "1+1") - self.assertEqual(clean_screen(reader.screen), "1+1") + self.assert_screen_equal(reader, "1+1", clean=True) def test_control_character(self): events = code_to_events("c\x1d\n") reader = self.prepare_reader(events) output = multiline_input(reader) self.assertEqual(output, "c\x1d") - self.assertEqual(clean_screen(reader.screen), "c") + self.assert_screen_equal(reader, "c\x1d", clean=True) def test_history_search_backward(self): # Test history search backward with "imp" input @@ -781,7 +781,7 @@ def test_history_search_backward(self): # search for "imp" in history output = multiline_input(reader) self.assertEqual(output, "import os") - self.assertEqual(clean_screen(reader.screen), "import os") + self.assert_screen_equal(reader, "import os", clean=True) def test_history_search_backward_empty(self): # Test history search backward with an empty input @@ -800,7 +800,7 @@ def test_history_search_backward_empty(self): # search backward in history output = multiline_input(reader) self.assertEqual(output, "import os") - self.assertEqual(clean_screen(reader.screen), "import os") + self.assert_screen_equal(reader, "import os", clean=True) class TestPyReplCompleter(TestCase): diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index 468c0c5e68d165..109cb603ae88b6 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -4,31 +4,28 @@ from unittest import TestCase from unittest.mock import MagicMock -from .support import handle_all_events, handle_events_narrow_console, code_to_events, prepare_reader, prepare_console +from .support import handle_all_events, handle_events_narrow_console +from .support import ScreenEqualMixin, code_to_events +from .support import prepare_reader, prepare_console from _pyrepl.console import Event from _pyrepl.reader import Reader -class TestReader(TestCase): - def assert_screen_equals(self, reader, expected): - actual = reader.screen - expected = expected.split("\n") - self.assertListEqual(actual, expected) - +class TestReader(ScreenEqualMixin, TestCase): def test_calc_screen_wrap_simple(self): events = code_to_events(10 * "a") reader, _ = handle_events_narrow_console(events) - self.assert_screen_equals(reader, f"{9*"a"}\\\na") + self.assert_screen_equal(reader, f"{9*"a"}\\\na") def test_calc_screen_wrap_wide_characters(self): events = code_to_events(8 * "a" + "樂") reader, _ = handle_events_narrow_console(events) - self.assert_screen_equals(reader, f"{8*"a"}\\\n樂") + self.assert_screen_equal(reader, f"{8*"a"}\\\n樂") def test_calc_screen_wrap_three_lines(self): events = code_to_events(20 * "a") reader, _ = handle_events_narrow_console(events) - self.assert_screen_equals(reader, f"{9*"a"}\\\n{9*"a"}\\\naa") + self.assert_screen_equal(reader, f"{9*"a"}\\\n{9*"a"}\\\naa") def test_calc_screen_prompt_handling(self): def prepare_reader_keep_prompts(*args, **kwargs): @@ -48,7 +45,7 @@ def prepare_reader_keep_prompts(*args, **kwargs): prepare_reader=prepare_reader_keep_prompts, ) # fmt: off - self.assert_screen_equals( + self.assert_screen_equal( reader, ( ">>> if so\\\n" @@ -74,13 +71,17 @@ def test_calc_screen_wrap_three_lines_mixed_character(self): reader, _ = handle_events_narrow_console(events) # fmt: off - self.assert_screen_equals(reader, ( - "def f():\n" - f" {7*"a"}\\\n" - "a\n" - f" {3*"樂"}\\\n" - "樂樂" - )) + self.assert_screen_equal( + reader, + ( + "def f():\n" + f" {7*"a"}\\\n" + "a\n" + f" {3*"樂"}\\\n" + "樂樂" + ), + clean=True, + ) # fmt: on def test_calc_screen_backspace(self): @@ -91,7 +92,7 @@ def test_calc_screen_backspace(self): ], ) reader, _ = handle_all_events(events) - self.assert_screen_equals(reader, "aa") + self.assert_screen_equal(reader, "aa") def test_calc_screen_wrap_removes_after_backspace(self): events = itertools.chain( @@ -101,7 +102,7 @@ def test_calc_screen_wrap_removes_after_backspace(self): ], ) reader, _ = handle_events_narrow_console(events) - self.assert_screen_equals(reader, 9 * "a") + self.assert_screen_equal(reader, 9 * "a") def test_calc_screen_backspace_in_second_line_after_wrap(self): events = itertools.chain( @@ -111,7 +112,7 @@ def test_calc_screen_backspace_in_second_line_after_wrap(self): ], ) reader, _ = handle_events_narrow_console(events) - self.assert_screen_equals(reader, f"{9*"a"}\\\na") + self.assert_screen_equal(reader, f"{9*"a"}\\\na") def test_setpos_for_xy_simple(self): events = code_to_events("11+11") @@ -123,7 +124,7 @@ def test_control_characters(self): code = 'flag = "🏳️‍🌈"' events = code_to_events(code) reader, _ = handle_all_events(events) - self.assert_screen_equals(reader, 'flag = "🏳️\\u200d🌈"') + self.assert_screen_equal(reader, 'flag = "🏳️\\u200d🌈"', clean=True) def test_setpos_from_xy_multiple_lines(self): # fmt: off @@ -173,7 +174,7 @@ def test_up_arrow_after_ctrl_r(self): ) reader, _ = handle_all_events(events) - self.assert_screen_equals(reader, "") + self.assert_screen_equal(reader, "") def test_newline_within_block_trailing_whitespace(self): # fmt: off @@ -212,13 +213,14 @@ def test_newline_within_block_trailing_whitespace(self): " \n" " a = 1\n" " \n" - " " # HistoricalReader will trim trailing whitespace + " " # HistoricalReader will trim trailing whitespace ) - self.assert_screen_equals(reader, expected) + self.assert_screen_equal(reader, expected, clean=True) self.assertTrue(reader.finished) def test_input_hook_is_called_if_set(self): input_hook = MagicMock() + def _prepare_console(events): console = MagicMock() console.get_event.side_effect = events @@ -235,18 +237,35 @@ def _prepare_console(events): def test_keyboard_interrupt_clears_screen(self): namespace = {"itertools": itertools} code = "import itertools\nitertools." - events = itertools.chain(code_to_events(code), [ - Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion - Event(evt='key', data='\t', raw=bytearray(b'\t')), - Event(evt='key', data='\x03', raw=bytearray(b'\x03')), # Ctrl-C - ]) - - completing_reader = functools.partial( - prepare_reader, - readline_completer=rlcompleter.Completer(namespace).complete + events = itertools.chain( + code_to_events(code), + [ + # Two tabs for completion + Event(evt="key", data="\t", raw=bytearray(b"\t")), + Event(evt="key", data="\t", raw=bytearray(b"\t")), + Event(evt="key", data="\x03", raw=bytearray(b"\x03")), # Ctrl-C + ], ) - reader, _ = handle_all_events(events, prepare_reader=completing_reader) - self.assertEqual(reader.calc_screen(), code.split("\n")) + console = prepare_console(events) + reader = prepare_reader( + console, + readline_completer=rlcompleter.Completer(namespace).complete, + ) + try: + # we're not using handle_all_events() here to be able to + # follow the KeyboardInterrupt sequence of events. Normally this + # happens in simple_interact.run_multiline_interactive_console. + while True: + reader.handle1() + except KeyboardInterrupt: + # at this point the completions are still visible + self.assertTrue(len(reader.screen) > 2) + reader.refresh() + # after the refresh, they are gone + self.assertEqual(len(reader.screen), 2) + self.assert_screen_equal(reader, code, clean=True) + else: + self.fail("KeyboardInterrupt not raised.") def test_prompt_length(self): # Handles simple ASCII prompt @@ -282,14 +301,19 @@ def test_prompt_length(self): def test_completions_updated_on_key_press(self): namespace = {"itertools": itertools} code = "itertools." - events = itertools.chain(code_to_events(code), [ - Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion - Event(evt='key', data='\t', raw=bytearray(b'\t')), - ], code_to_events("a")) + events = itertools.chain( + code_to_events(code), + [ + # Two tabs for completion + Event(evt="key", data="\t", raw=bytearray(b"\t")), + Event(evt="key", data="\t", raw=bytearray(b"\t")), + ], + code_to_events("a"), + ) completing_reader = functools.partial( prepare_reader, - readline_completer=rlcompleter.Completer(namespace).complete + readline_completer=rlcompleter.Completer(namespace).complete, ) reader, _ = handle_all_events(events, prepare_reader=completing_reader) @@ -301,17 +325,21 @@ def test_completions_updated_on_key_press(self): def test_key_press_on_tab_press_once(self): namespace = {"itertools": itertools} code = "itertools." - events = itertools.chain(code_to_events(code), [ - Event(evt='key', data='\t', raw=bytearray(b'\t')), - ], code_to_events("a")) + events = itertools.chain( + code_to_events(code), + [ + Event(evt="key", data="\t", raw=bytearray(b"\t")), + ], + code_to_events("a"), + ) completing_reader = functools.partial( prepare_reader, - readline_completer=rlcompleter.Completer(namespace).complete + readline_completer=rlcompleter.Completer(namespace).complete, ) reader, _ = handle_all_events(events, prepare_reader=completing_reader) - self.assert_screen_equals(reader, f"{code}a") + self.assert_screen_equal(reader, f"{code}a") def test_pos2xy_with_no_columns(self): console = prepare_console([]) diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index 15dbf48bcf0f1c..057cdd112852dc 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -7,7 +7,7 @@ from unittest import TestCase from unittest.mock import MagicMock, call, patch, ANY -from .support import handle_all_events, code_to_events +from .support import handle_all_events, code_to_events, reader_no_colors try: from _pyrepl.console import Event @@ -252,7 +252,9 @@ def test_resize_bigger_on_multiline_function(self, _os_write): # fmt: on events = itertools.chain(code_to_events(code)) - reader, console = handle_events_short_unix_console(events) + reader, console = handle_events_short_unix_console( + events, prepare_reader=reader_no_colors + ) console.height = 2 console.getheightwidth = MagicMock(lambda _: (2, 80)) diff --git a/Misc/mypy/README.md b/Misc/mypy/README.md new file mode 100644 index 00000000000000..05eda6c0b82f0a --- /dev/null +++ b/Misc/mypy/README.md @@ -0,0 +1,16 @@ +# Mypy path symlinks + +This directory stores symlinks to standard library modules and packages +that are fully type-annotated and ready to be used in type checking of +the rest of the stdlib or Tools/ and so on. + +Due to most of the standard library being untyped, we prefer not to +point mypy directly at `Lib/` for type checking. Additionally, mypy +as a tool does not support shadowing typing-related standard libraries +like `types`, `typing`, and `collections.abc`. + +So instead, we set `mypy_path` to include this directory, +which only links modules and packages we know are safe to be +type-checked themselves and used as dependencies. + +See `Lib/_pyrepl/mypy.ini` for an example. \ No newline at end of file diff --git a/Misc/mypy/_colorize.py b/Misc/mypy/_colorize.py new file mode 120000 index 00000000000000..9b7304769ec30b --- /dev/null +++ b/Misc/mypy/_colorize.py @@ -0,0 +1 @@ +../../Lib/_colorize.py \ No newline at end of file diff --git a/Misc/mypy/_pyrepl b/Misc/mypy/_pyrepl new file mode 120000 index 00000000000000..bd7b69909663b6 --- /dev/null +++ b/Misc/mypy/_pyrepl @@ -0,0 +1 @@ +../../Lib/_pyrepl \ No newline at end of file