mirror of
https://github.com/python/cpython.git
synced 2025-07-31 23:23:11 +00:00
[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:
parent
0a22407a23
commit
095c1263eb
14 changed files with 234 additions and 133 deletions
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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(
|
||||||
|
reader,
|
||||||
|
(
|
||||||
"def f():\n"
|
"def f():\n"
|
||||||
f" {7*"a"}\\\n"
|
f" {7*"a"}\\\n"
|
||||||
"a\n"
|
"a\n"
|
||||||
f" {3*"樂"}\\\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
|
||||||
|
@ -214,11 +215,12 @@ class TestReader(TestCase):
|
||||||
" \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([])
|
||||||
|
|
|
@ -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
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