https://github.com/python/cpython/commit/095c1263eb03429e8b71e5efa3c3ba35785dfc2e
commit: 095c1263eb03429e8b71e5efa3c3ba35785dfc2e
branch: 3.13
author: Łukasz Langa <luk...@langa.pl>
committer: ambv <luk...@langa.pl>
date: 2025-03-21T17:25:45+01:00
summary:

[3.13] gh-131507: Clean up tests and type checking for `_pyrepl` (GH-131509) 
(GH-131546)

(cherry picked from commit 5d8e981c8477ce483374b2fe6cd309a08c956299)

files:
A Misc/mypy/README.md
A Misc/mypy/_colorize.py
A Misc/mypy/_pyrepl
M Lib/_colorize.py
M Lib/_pyrepl/commands.py
M Lib/_pyrepl/console.py
M Lib/_pyrepl/mypy.ini
M Lib/_pyrepl/reader.py
M Lib/_pyrepl/readline.py
M Lib/_pyrepl/utils.py
M Lib/test/test_pyrepl/support.py
M Lib/test/test_pyrepl/test_pyrepl.py
M Lib/test/test_pyrepl/test_reader.py
M Lib/test/test_pyrepl/test_unix_console.py

diff --git a/Lib/_colorize.py b/Lib/_colorize.py
index 70acfd4ad0ba8f..9eb6f0933b8150 100644
--- a/Lib/_colorize.py
+++ b/Lib/_colorize.py
@@ -1,21 +1,64 @@
+from __future__ import annotations
 import io
 import os
 import sys
 
 COLORIZE = True
 
+# types
+if False:
+    from typing import IO
+
 
 class ANSIColors:
-    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()
 
@@ -24,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
 
@@ -64,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 df2b4222640829..3c1ca6f682659d 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
@@ -160,7 +160,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 68e2a9d4bd29e3..5c3cdd843c2fa8 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 45e3bf758f17de..e8cf5812f3b3ef 100644
--- a/Lib/test/test_pyrepl/support.py
+++ b/Lib/test/test_pyrepl/support.py
@@ -7,14 +7,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
@@ -39,18 +49,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):
@@ -100,6 +114,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 191fce3f7ae6d5..6e2c47c33623bc 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 <page up> 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 <page up> 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 270d4d4fb8ae65..40d9c2f91b101a 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

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to