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

(cherry picked from commit 5d8e981c84)
This commit is contained in:
Łukasz Langa 2025-03-21 17:25:45 +01:00 committed by GitHub
parent 0a22407a23
commit 095c1263eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 234 additions and 133 deletions

View file

@ -1,20 +1,63 @@
from __future__ import annotations
import io import io
import os import os
import sys import sys
COLORIZE = True COLORIZE = True
# types
if False:
from typing import IO
class ANSIColors: class ANSIColors:
RESET = "\x1b[0m"
BLACK = "\x1b[30m"
BLUE = "\x1b[34m"
CYAN = "\x1b[36m"
GREEN = "\x1b[32m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m"
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_GREEN = "\x1b[1;32m"
BOLD_MAGENTA = "\x1b[1;35m" BOLD_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m" BOLD_RED = "\x1b[1;31m"
GREEN = "\x1b[32m" BOLD_WHITE = "\x1b[1;37m" # actual WHITE
GREY = "\x1b[90m" BOLD_YELLOW = "\x1b[1;33m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m" # intense = like bold but without being bold
RESET = "\x1b[0m" INTENSE_BLACK = "\x1b[90m"
YELLOW = "\x1b[33m" 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() NoColors = ANSIColors()
@ -24,14 +67,16 @@ for attr in dir(NoColors):
setattr(NoColors, attr, "") 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): if colorize or can_colorize(file=file):
return ANSIColors() return ANSIColors()
else: else:
return NoColors return NoColors
def can_colorize(*, file=None) -> bool: def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
if file is None: if file is None:
file = sys.stdout file = sys.stdout
@ -64,4 +109,4 @@ def can_colorize(*, file=None) -> bool:
try: try:
return os.isatty(file.fileno()) return os.isatty(file.fileno())
except io.UnsupportedOperation: except io.UnsupportedOperation:
return file.isatty() return hasattr(file, "isatty") and file.isatty()

View file

@ -456,7 +456,7 @@ class invalid_command(Command):
class show_history(Command): class show_history(Command):
def do(self) -> None: def do(self) -> None:
from .pager import get_pager from .pager import get_pager
from site import gethistoryfile # type: ignore[attr-defined] from site import gethistoryfile
history = os.linesep.join(self.reader.history[:]) history = os.linesep.join(self.reader.history[:])
self.reader.console.restore() self.reader.console.restore()

View file

@ -19,7 +19,7 @@
from __future__ import annotations from __future__ import annotations
import _colorize # type: ignore[import-not-found] import _colorize
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import ast import ast
@ -160,7 +160,7 @@ class InteractiveColoredConsole(code.InteractiveConsole):
*, *,
local_exit: bool = False, local_exit: bool = False,
) -> None: ) -> 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() self.can_colorize = _colorize.can_colorize()
def showsyntaxerror(self, filename=None, **kwargs): def showsyntaxerror(self, filename=None, **kwargs):

View file

@ -4,8 +4,9 @@
[mypy] [mypy]
files = Lib/_pyrepl files = Lib/_pyrepl
mypy_path = $MYPY_CONFIG_FILE_DIR/../../Misc/mypy
explicit_package_bases = True explicit_package_bases = True
python_version = 3.12 python_version = 3.13
platform = linux platform = linux
pretty = True pretty = True
@ -22,3 +23,7 @@ check_untyped_defs = False
# Various internal modules that typeshed deliberately doesn't have stubs for: # Various internal modules that typeshed deliberately doesn't have stubs for:
[mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*] [mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*]
ignore_missing_imports = True ignore_missing_imports = True
# Other untyped parts of the stdlib
[mypy-idlelib.*]
ignore_missing_imports = True

View file

@ -26,11 +26,11 @@ import sys
from contextlib import contextmanager from contextlib import contextmanager
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
import unicodedata 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 . import commands, console, input
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width from .utils import wlen, unbracket, str_width
from .trace import trace from .trace import trace
@ -421,42 +421,15 @@ class Reader:
@staticmethod @staticmethod
def process_prompt(prompt: str) -> tuple[str, int]: 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 The prompt string has the zero-width brackets recognized by shells
and \x02 are used to bracket ANSI control sequences and need to be (\x01 and \x02) removed. The length ignores anything between those
excluded from the length calculation. So also a copy of the prompt brackets as well as any ANSI escape sequences.
is returned with these control characters removed.""" """
out_prompt = unbracket(prompt, including_content=False)
# The logic below also ignores the length of common escape visible_prompt = unbracket(prompt, including_content=True)
# sequences if they were not explicitly within \x01...\x02. return out_prompt, wlen(visible_prompt)
# 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
def bow(self, p: int | None = None) -> int: def bow(self, p: int | None = None) -> int:
"""Return the 0-based index of the word break preceding p most """Return the 0-based index of the word break preceding p most

View file

@ -32,7 +32,7 @@ import warnings
from dataclasses import dataclass, field from dataclasses import dataclass, field
import os import os
from site import gethistoryfile # type: ignore[attr-defined] from site import gethistoryfile
import sys import sys
from rlcompleter import Completer as RLCompleter from rlcompleter import Completer as RLCompleter

View file

@ -3,6 +3,8 @@ import unicodedata
import functools import functools
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") 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 @functools.cache
@ -10,16 +12,27 @@ def str_width(c: str) -> int:
if ord(c) < 128: if ord(c) < 128:
return 1 return 1
w = unicodedata.east_asian_width(c) w = unicodedata.east_asian_width(c)
if w in ('N', 'Na', 'H', 'A'): if w in ("N", "Na", "H", "A"):
return 1 return 1
return 2 return 2
def wlen(s: str) -> int: def wlen(s: str) -> int:
if len(s) == 1 and s != '\x1a': if len(s) == 1 and s != "\x1a":
return str_width(s) return str_width(s)
length = sum(str_width(i) for i in s) length = sum(str_width(i) for i in s)
# remove lengths of any escape sequences # remove lengths of any escape sequences
sequence = ANSI_ESCAPE_SEQUENCE.findall(s) 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 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)

View file

@ -7,14 +7,24 @@ from unittest.mock import MagicMock
from _pyrepl.console import Console, Event from _pyrepl.console import Console, Event
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
from _pyrepl.simple_interact import _strip_final_indent 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): def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None):
saved = reader.more_lines saved = reader.more_lines
try: try:
reader.more_lines = partial(more_lines, namespace=namespace) reader.more_lines = partial(more_lines, namespace=namespace)
reader.ps1 = reader.ps2 = ">>>" reader.ps1 = reader.ps2 = ">>> "
reader.ps3 = reader.ps4 = "..." reader.ps3 = reader.ps4 = "... "
return reader.readline() return reader.readline()
finally: finally:
reader.more_lines = saved 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"))) 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. """Cleans color and console characters out of a screen output.
This is useful for screen testing, it increases the test readability since This is useful for screen testing, it increases the test readability since
it strips out all the unreadable side of the screen. it strips out all the unreadable side of the screen.
""" """
output = [] output = []
for line in screen: for line in reader.screen:
if line.startswith(">>>") or line.startswith("..."): line = unbracket(line, including_content=True)
line = line[3:] 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) output.append(line)
return "\n".join(output).strip() return output
def prepare_reader(console: Console, **kwargs): def prepare_reader(console: Console, **kwargs):
@ -100,6 +114,9 @@ handle_events_narrow_console = partial(
prepare_console=partial(prepare_console, width=10), 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): class FakeConsole(Console):
def __init__(self, events, encoding="utf-8") -> None: def __init__(self, events, encoding="utf-8") -> None:

View file

@ -17,12 +17,12 @@ from test.support.os_helper import unlink
from .support import ( from .support import (
FakeConsole, FakeConsole,
ScreenEqualMixin,
handle_all_events, handle_all_events,
handle_events_narrow_console, handle_events_narrow_console,
more_lines, more_lines,
multiline_input, multiline_input,
code_to_events, code_to_events,
clean_screen,
) )
from _pyrepl.console import Event from _pyrepl.console import Event
from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig, from _pyrepl.readline import (ReadlineAlikeReader, ReadlineConfig,
@ -587,7 +587,7 @@ class TestPyReplAutoindent(TestCase):
self.assertEqual(output, output_code) self.assertEqual(output, output_code)
class TestPyReplOutput(TestCase): class TestPyReplOutput(ScreenEqualMixin, TestCase):
def prepare_reader(self, events): def prepare_reader(self, events):
console = FakeConsole(events) console = FakeConsole(events)
config = ReadlineConfig(readline_completer=None) config = ReadlineConfig(readline_completer=None)
@ -620,7 +620,7 @@ class TestPyReplOutput(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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): def test_get_line_buffer_returns_str(self):
reader = self.prepare_reader(code_to_events("\n")) reader = self.prepare_reader(code_to_events("\n"))
@ -654,11 +654,13 @@ class TestPyReplOutput(TestCase):
reader = self.prepare_reader(events) reader = self.prepare_reader(events)
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "def f():\n ...\n ") expected = "def f():\n ...\n "
self.assertEqual(clean_screen(reader.screen), "def f():\n ...") self.assertEqual(output, expected)
self.assert_screen_equal(reader, expected, clean=True)
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "def g():\n pass\n ") expected = "def g():\n pass\n "
self.assertEqual(clean_screen(reader.screen), "def g():\n pass") self.assertEqual(output, expected)
self.assert_screen_equal(reader, expected, clean=True)
def test_history_navigation_with_up_arrow(self): def test_history_navigation_with_up_arrow(self):
events = itertools.chain( events = itertools.chain(
@ -677,16 +679,16 @@ class TestPyReplOutput(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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) output = multiline_input(reader)
self.assertEqual(output, "2+2") 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) output = multiline_input(reader)
self.assertEqual(output, "2+2") 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) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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): def test_history_with_multiline_entries(self):
code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n" code = "def foo():\nx = 1\ny = 2\nz = 3\n\ndef bar():\nreturn 42\n\n"
@ -705,11 +707,9 @@ class TestPyReplOutput(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
output = multiline_input(reader) output = multiline_input(reader)
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual( expected = "def foo():\n x = 1\n y = 2\n z = 3\n "
clean_screen(reader.screen), self.assert_screen_equal(reader, expected, clean=True)
'def foo():\n x = 1\n y = 2\n z = 3' self.assertEqual(output, expected)
)
self.assertEqual(output, "def foo():\n x = 1\n y = 2\n z = 3\n ")
def test_history_navigation_with_down_arrow(self): def test_history_navigation_with_down_arrow(self):
@ -728,7 +728,7 @@ class TestPyReplOutput(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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): def test_history_search(self):
events = itertools.chain( events = itertools.chain(
@ -745,23 +745,23 @@ class TestPyReplOutput(TestCase):
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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) output = multiline_input(reader)
self.assertEqual(output, "2+2") 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) output = multiline_input(reader)
self.assertEqual(output, "3+3") 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) output = multiline_input(reader)
self.assertEqual(output, "1+1") 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): def test_control_character(self):
events = code_to_events("c\x1d\n") events = code_to_events("c\x1d\n")
reader = self.prepare_reader(events) reader = self.prepare_reader(events)
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "c\x1d") 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): def test_history_search_backward(self):
# Test <page up> history search backward with "imp" input # Test <page up> history search backward with "imp" input
@ -781,7 +781,7 @@ class TestPyReplOutput(TestCase):
# search for "imp" in history # search for "imp" in history
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "import os") 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): def test_history_search_backward_empty(self):
# Test <page up> history search backward with an empty input # Test <page up> history search backward with an empty input
@ -800,7 +800,7 @@ class TestPyReplOutput(TestCase):
# search backward in history # search backward in history
output = multiline_input(reader) output = multiline_input(reader)
self.assertEqual(output, "import os") 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): class TestPyReplCompleter(TestCase):

View file

@ -4,31 +4,28 @@ import rlcompleter
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock 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.console import Event
from _pyrepl.reader import Reader from _pyrepl.reader import Reader
class TestReader(TestCase): class TestReader(ScreenEqualMixin, TestCase):
def assert_screen_equals(self, reader, expected):
actual = reader.screen
expected = expected.split("\n")
self.assertListEqual(actual, expected)
def test_calc_screen_wrap_simple(self): def test_calc_screen_wrap_simple(self):
events = code_to_events(10 * "a") events = code_to_events(10 * "a")
reader, _ = handle_events_narrow_console(events) 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): def test_calc_screen_wrap_wide_characters(self):
events = code_to_events(8 * "a" + "") events = code_to_events(8 * "a" + "")
reader, _ = handle_events_narrow_console(events) 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): def test_calc_screen_wrap_three_lines(self):
events = code_to_events(20 * "a") events = code_to_events(20 * "a")
reader, _ = handle_events_narrow_console(events) 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 test_calc_screen_prompt_handling(self):
def prepare_reader_keep_prompts(*args, **kwargs): def prepare_reader_keep_prompts(*args, **kwargs):
@ -48,7 +45,7 @@ class TestReader(TestCase):
prepare_reader=prepare_reader_keep_prompts, prepare_reader=prepare_reader_keep_prompts,
) )
# fmt: off # fmt: off
self.assert_screen_equals( self.assert_screen_equal(
reader, reader,
( (
">>> if so\\\n" ">>> if so\\\n"
@ -74,13 +71,17 @@ class TestReader(TestCase):
reader, _ = handle_events_narrow_console(events) reader, _ = handle_events_narrow_console(events)
# fmt: off # fmt: off
self.assert_screen_equals(reader, ( self.assert_screen_equal(
"def f():\n" reader,
f" {7*"a"}\\\n" (
"a\n" "def f():\n"
f" {3*""}\\\n" f" {7*"a"}\\\n"
"樂樂" "a\n"
)) f" {3*""}\\\n"
"樂樂"
),
clean=True,
)
# fmt: on # fmt: on
def test_calc_screen_backspace(self): def test_calc_screen_backspace(self):
@ -91,7 +92,7 @@ class TestReader(TestCase):
], ],
) )
reader, _ = handle_all_events(events) 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): def test_calc_screen_wrap_removes_after_backspace(self):
events = itertools.chain( events = itertools.chain(
@ -101,7 +102,7 @@ class TestReader(TestCase):
], ],
) )
reader, _ = handle_events_narrow_console(events) 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): def test_calc_screen_backspace_in_second_line_after_wrap(self):
events = itertools.chain( events = itertools.chain(
@ -111,7 +112,7 @@ class TestReader(TestCase):
], ],
) )
reader, _ = handle_events_narrow_console(events) 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): def test_setpos_for_xy_simple(self):
events = code_to_events("11+11") events = code_to_events("11+11")
@ -123,7 +124,7 @@ class TestReader(TestCase):
code = 'flag = "🏳️‍🌈"' code = 'flag = "🏳️‍🌈"'
events = code_to_events(code) events = code_to_events(code)
reader, _ = handle_all_events(events) 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): def test_setpos_from_xy_multiple_lines(self):
# fmt: off # fmt: off
@ -173,7 +174,7 @@ class TestReader(TestCase):
) )
reader, _ = handle_all_events(events) reader, _ = handle_all_events(events)
self.assert_screen_equals(reader, "") self.assert_screen_equal(reader, "")
def test_newline_within_block_trailing_whitespace(self): def test_newline_within_block_trailing_whitespace(self):
# fmt: off # fmt: off
@ -212,13 +213,14 @@ class TestReader(TestCase):
" \n" " \n"
" a = 1\n" " a = 1\n"
" \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) self.assertTrue(reader.finished)
def test_input_hook_is_called_if_set(self): def test_input_hook_is_called_if_set(self):
input_hook = MagicMock() input_hook = MagicMock()
def _prepare_console(events): def _prepare_console(events):
console = MagicMock() console = MagicMock()
console.get_event.side_effect = events console.get_event.side_effect = events
@ -235,18 +237,35 @@ class TestReader(TestCase):
def test_keyboard_interrupt_clears_screen(self): def test_keyboard_interrupt_clears_screen(self):
namespace = {"itertools": itertools} namespace = {"itertools": itertools}
code = "import itertools\nitertools." code = "import itertools\nitertools."
events = itertools.chain(code_to_events(code), [ events = itertools.chain(
Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion code_to_events(code),
Event(evt='key', data='\t', raw=bytearray(b'\t')), [
Event(evt='key', data='\x03', raw=bytearray(b'\x03')), # Ctrl-C # Two tabs for completion
]) Event(evt="key", data="\t", raw=bytearray(b"\t")),
Event(evt="key", data="\t", raw=bytearray(b"\t")),
completing_reader = functools.partial( Event(evt="key", data="\x03", raw=bytearray(b"\x03")), # Ctrl-C
prepare_reader, ],
readline_completer=rlcompleter.Completer(namespace).complete
) )
reader, _ = handle_all_events(events, prepare_reader=completing_reader) console = prepare_console(events)
self.assertEqual(reader.calc_screen(), code.split("\n")) 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): def test_prompt_length(self):
# Handles simple ASCII prompt # Handles simple ASCII prompt
@ -282,14 +301,19 @@ class TestReader(TestCase):
def test_completions_updated_on_key_press(self): def test_completions_updated_on_key_press(self):
namespace = {"itertools": itertools} namespace = {"itertools": itertools}
code = "itertools." code = "itertools."
events = itertools.chain(code_to_events(code), [ events = itertools.chain(
Event(evt='key', data='\t', raw=bytearray(b'\t')), # Two tabs for completion code_to_events(code),
Event(evt='key', data='\t', raw=bytearray(b'\t')), [
], code_to_events("a")) # 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( completing_reader = functools.partial(
prepare_reader, prepare_reader,
readline_completer=rlcompleter.Completer(namespace).complete readline_completer=rlcompleter.Completer(namespace).complete,
) )
reader, _ = handle_all_events(events, prepare_reader=completing_reader) reader, _ = handle_all_events(events, prepare_reader=completing_reader)
@ -301,17 +325,21 @@ class TestReader(TestCase):
def test_key_press_on_tab_press_once(self): def test_key_press_on_tab_press_once(self):
namespace = {"itertools": itertools} namespace = {"itertools": itertools}
code = "itertools." code = "itertools."
events = itertools.chain(code_to_events(code), [ events = itertools.chain(
Event(evt='key', data='\t', raw=bytearray(b'\t')), code_to_events(code),
], code_to_events("a")) [
Event(evt="key", data="\t", raw=bytearray(b"\t")),
],
code_to_events("a"),
)
completing_reader = functools.partial( completing_reader = functools.partial(
prepare_reader, prepare_reader,
readline_completer=rlcompleter.Completer(namespace).complete readline_completer=rlcompleter.Completer(namespace).complete,
) )
reader, _ = handle_all_events(events, prepare_reader=completing_reader) 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): def test_pos2xy_with_no_columns(self):
console = prepare_console([]) console = prepare_console([])

View file

@ -7,7 +7,7 @@ from test.support import os_helper
from unittest import TestCase from unittest import TestCase
from unittest.mock import MagicMock, call, patch, ANY 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: try:
from _pyrepl.console import Event from _pyrepl.console import Event
@ -252,7 +252,9 @@ class TestConsole(TestCase):
# fmt: on # fmt: on
events = itertools.chain(code_to_events(code)) 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.height = 2
console.getheightwidth = MagicMock(lambda _: (2, 80)) console.getheightwidth = MagicMock(lambda _: (2, 80))

16
Misc/mypy/README.md Normal file
View file

@ -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.

1
Misc/mypy/_colorize.py Symbolic link
View file

@ -0,0 +1 @@
../../Lib/_colorize.py

1
Misc/mypy/_pyrepl Symbolic link
View file

@ -0,0 +1 @@
../../Lib/_pyrepl