mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
gh-131507: Clean up tests and type checking for _pyrepl
(#131509)
This commit is contained in:
parent
d3f6063af1
commit
5d8e981c84
14 changed files with 234 additions and 135 deletions
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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([])
|
||||
|
|
|
@ -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
16
Misc/mypy/README.md
Normal 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
1
Misc/mypy/_colorize.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../Lib/_colorize.py
|
1
Misc/mypy/_pyrepl
Symbolic link
1
Misc/mypy/_pyrepl
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../Lib/_pyrepl
|
Loading…
Add table
Add a link
Reference in a new issue