gh-131507: Clean up tests and type checking for _pyrepl (#131509)

This commit is contained in:
Łukasz Langa 2025-03-21 15:48:10 +01:00 committed by GitHub
parent d3f6063af1
commit 5d8e981c84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 234 additions and 135 deletions

View file

@ -1,22 +1,63 @@
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"
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_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m"
BLACK = "\x1b[30m"
GREEN = "\x1b[32m"
GREY = "\x1b[90m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m"
RESET = "\x1b[0m"
YELLOW = "\x1b[33m"
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 @@ for attr in dir(NoColors):
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()

View file

@ -456,7 +456,7 @@ class invalid_command(Command):
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()

View file

@ -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 @@ class InteractiveColoredConsole(code.InteractiveConsole):
*,
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):

View file

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

View file

@ -26,11 +26,11 @@ import sys
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 @@ class Reader:
@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

View file

@ -32,7 +32,7 @@ import warnings
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

View file

@ -3,6 +3,8 @@ import unicodedata
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)

View file

@ -6,14 +6,24 @@ from unittest.mock import MagicMock
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 @@ handle_events_narrow_console = partial(
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:

View file

@ -17,12 +17,12 @@ from test.support.os_helper import unlink
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 @@ class TestPyReplAutoindent(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
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 @@ class TestPyReplOutput(TestCase):
# 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 @@ class TestPyReplOutput(TestCase):
# 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):

View file

@ -4,31 +4,28 @@ import rlcompleter
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 @@ class TestReader(TestCase):
prepare_reader=prepare_reader_keep_prompts,
)
# fmt: off
self.assert_screen_equals(
self.assert_screen_equal(
reader,
(
">>> if so\\\n"
@ -74,13 +71,17 @@ class TestReader(TestCase):
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 @@ class TestReader(TestCase):
],
)
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 @@ class TestReader(TestCase):
],
)
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 @@ class TestReader(TestCase):
],
)
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 @@ class TestReader(TestCase):
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 @@ class TestReader(TestCase):
)
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 @@ class TestReader(TestCase):
" \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 @@ class TestReader(TestCase):
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 @@ class TestReader(TestCase):
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 @@ class TestReader(TestCase):
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([])

View file

@ -7,7 +7,7 @@ from test.support import os_helper
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 @@ class TestConsole(TestCase):
# 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))

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