Skip to content

Commit 5d8e981

Browse files
authored
gh-131507: Clean up tests and type checking for _pyrepl (#131509)
1 parent d3f6063 commit 5d8e981

14 files changed

+232
-133
lines changed

Diff for: Lib/_colorize.py

+52-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,64 @@
1+
from __future__ import annotations
12
import io
23
import os
34
import sys
45

56
COLORIZE = True
67

8+
# types
9+
if False:
10+
from typing import IO
11+
712

813
class ANSIColors:
9-
BACKGROUND_YELLOW = "\x1b[43m"
10-
BOLD_GREEN = "\x1b[1;32m"
11-
BOLD_MAGENTA = "\x1b[1;35m"
12-
BOLD_RED = "\x1b[1;31m"
14+
RESET = "\x1b[0m"
15+
1316
BLACK = "\x1b[30m"
17+
BLUE = "\x1b[34m"
18+
CYAN = "\x1b[36m"
1419
GREEN = "\x1b[32m"
15-
GREY = "\x1b[90m"
1620
MAGENTA = "\x1b[35m"
1721
RED = "\x1b[31m"
18-
RESET = "\x1b[0m"
22+
WHITE = "\x1b[37m" # more like LIGHT GRAY
1923
YELLOW = "\x1b[33m"
2024

25+
BOLD_BLACK = "\x1b[1;30m" # DARK GRAY
26+
BOLD_BLUE = "\x1b[1;34m"
27+
BOLD_CYAN = "\x1b[1;36m"
28+
BOLD_GREEN = "\x1b[1;32m"
29+
BOLD_MAGENTA = "\x1b[1;35m"
30+
BOLD_RED = "\x1b[1;31m"
31+
BOLD_WHITE = "\x1b[1;37m" # actual WHITE
32+
BOLD_YELLOW = "\x1b[1;33m"
33+
34+
# intense = like bold but without being bold
35+
INTENSE_BLACK = "\x1b[90m"
36+
INTENSE_BLUE = "\x1b[94m"
37+
INTENSE_CYAN = "\x1b[96m"
38+
INTENSE_GREEN = "\x1b[92m"
39+
INTENSE_MAGENTA = "\x1b[95m"
40+
INTENSE_RED = "\x1b[91m"
41+
INTENSE_WHITE = "\x1b[97m"
42+
INTENSE_YELLOW = "\x1b[93m"
43+
44+
BACKGROUND_BLACK = "\x1b[40m"
45+
BACKGROUND_BLUE = "\x1b[44m"
46+
BACKGROUND_CYAN = "\x1b[46m"
47+
BACKGROUND_GREEN = "\x1b[42m"
48+
BACKGROUND_MAGENTA = "\x1b[45m"
49+
BACKGROUND_RED = "\x1b[41m"
50+
BACKGROUND_WHITE = "\x1b[47m"
51+
BACKGROUND_YELLOW = "\x1b[43m"
52+
53+
INTENSE_BACKGROUND_BLACK = "\x1b[100m"
54+
INTENSE_BACKGROUND_BLUE = "\x1b[104m"
55+
INTENSE_BACKGROUND_CYAN = "\x1b[106m"
56+
INTENSE_BACKGROUND_GREEN = "\x1b[102m"
57+
INTENSE_BACKGROUND_MAGENTA = "\x1b[105m"
58+
INTENSE_BACKGROUND_RED = "\x1b[101m"
59+
INTENSE_BACKGROUND_WHITE = "\x1b[107m"
60+
INTENSE_BACKGROUND_YELLOW = "\x1b[103m"
61+
2162

2263
NoColors = ANSIColors()
2364

@@ -26,14 +67,16 @@ class ANSIColors:
2667
setattr(NoColors, attr, "")
2768

2869

29-
def get_colors(colorize: bool = False, *, file=None) -> ANSIColors:
70+
def get_colors(
71+
colorize: bool = False, *, file: IO[str] | IO[bytes] | None = None
72+
) -> ANSIColors:
3073
if colorize or can_colorize(file=file):
3174
return ANSIColors()
3275
else:
3376
return NoColors
3477

3578

36-
def can_colorize(*, file=None) -> bool:
79+
def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
3780
if file is None:
3881
file = sys.stdout
3982

@@ -66,4 +109,4 @@ def can_colorize(*, file=None) -> bool:
66109
try:
67110
return os.isatty(file.fileno())
68111
except io.UnsupportedOperation:
69-
return file.isatty()
112+
return hasattr(file, "isatty") and file.isatty()

Diff for: Lib/_pyrepl/commands.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ def do(self) -> None:
456456
class show_history(Command):
457457
def do(self) -> None:
458458
from .pager import get_pager
459-
from site import gethistoryfile # type: ignore[attr-defined]
459+
from site import gethistoryfile
460460

461461
history = os.linesep.join(self.reader.history[:])
462462
self.reader.console.restore()

Diff for: Lib/_pyrepl/console.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from __future__ import annotations
2121

22-
import _colorize # type: ignore[import-not-found]
22+
import _colorize
2323

2424
from abc import ABC, abstractmethod
2525
import ast
@@ -162,7 +162,7 @@ def __init__(
162162
*,
163163
local_exit: bool = False,
164164
) -> None:
165-
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
165+
super().__init__(locals=locals, filename=filename, local_exit=local_exit)
166166
self.can_colorize = _colorize.can_colorize()
167167

168168
def showsyntaxerror(self, filename=None, **kwargs):

Diff for: Lib/_pyrepl/mypy.ini

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
[mypy]
66
files = Lib/_pyrepl
7+
mypy_path = $MYPY_CONFIG_FILE_DIR/../../Misc/mypy
78
explicit_package_bases = True
8-
python_version = 3.12
9+
python_version = 3.13
910
platform = linux
1011
pretty = True
1112

@@ -22,3 +23,7 @@ check_untyped_defs = False
2223
# Various internal modules that typeshed deliberately doesn't have stubs for:
2324
[mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*]
2425
ignore_missing_imports = True
26+
27+
# Other untyped parts of the stdlib
28+
[mypy-idlelib.*]
29+
ignore_missing_imports = True

Diff for: Lib/_pyrepl/reader.py

+10-37
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
from contextlib import contextmanager
2727
from dataclasses import dataclass, field, fields
2828
import unicodedata
29-
from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found]
29+
from _colorize import can_colorize, ANSIColors
3030

3131

3232
from . import commands, console, input
33-
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width
33+
from .utils import wlen, unbracket, str_width
3434
from .trace import trace
3535

3636

@@ -421,42 +421,15 @@ def calc_screen(self) -> list[str]:
421421

422422
@staticmethod
423423
def process_prompt(prompt: str) -> tuple[str, int]:
424-
"""Process the prompt.
424+
r"""Return a tuple with the prompt string and its visible length.
425425
426-
This means calculate the length of the prompt. The character \x01
427-
and \x02 are used to bracket ANSI control sequences and need to be
428-
excluded from the length calculation. So also a copy of the prompt
429-
is returned with these control characters removed."""
430-
431-
# The logic below also ignores the length of common escape
432-
# sequences if they were not explicitly within \x01...\x02.
433-
# They are CSI (or ANSI) sequences ( ESC [ ... LETTER )
434-
435-
# wlen from utils already excludes ANSI_ESCAPE_SEQUENCE chars,
436-
# which breaks the logic below so we redefine it here.
437-
def wlen(s: str) -> int:
438-
return sum(str_width(i) for i in s)
439-
440-
out_prompt = ""
441-
l = wlen(prompt)
442-
pos = 0
443-
while True:
444-
s = prompt.find("\x01", pos)
445-
if s == -1:
446-
break
447-
e = prompt.find("\x02", s)
448-
if e == -1:
449-
break
450-
# Found start and end brackets, subtract from string length
451-
l = l - (e - s + 1)
452-
keep = prompt[pos:s]
453-
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
454-
out_prompt += keep + prompt[s + 1 : e]
455-
pos = e + 1
456-
keep = prompt[pos:]
457-
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
458-
out_prompt += keep
459-
return out_prompt, l
426+
The prompt string has the zero-width brackets recognized by shells
427+
(\x01 and \x02) removed. The length ignores anything between those
428+
brackets as well as any ANSI escape sequences.
429+
"""
430+
out_prompt = unbracket(prompt, including_content=False)
431+
visible_prompt = unbracket(prompt, including_content=True)
432+
return out_prompt, wlen(visible_prompt)
460433

461434
def bow(self, p: int | None = None) -> int:
462435
"""Return the 0-based index of the word break preceding p most

Diff for: Lib/_pyrepl/readline.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from dataclasses import dataclass, field
3333

3434
import os
35-
from site import gethistoryfile # type: ignore[attr-defined]
35+
from site import gethistoryfile
3636
import sys
3737
from rlcompleter import Completer as RLCompleter
3838

Diff for: Lib/_pyrepl/utils.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,36 @@
33
import functools
44

55
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
6+
ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02")
7+
ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})
68

79

810
@functools.cache
911
def str_width(c: str) -> int:
1012
if ord(c) < 128:
1113
return 1
1214
w = unicodedata.east_asian_width(c)
13-
if w in ('N', 'Na', 'H', 'A'):
15+
if w in ("N", "Na", "H", "A"):
1416
return 1
1517
return 2
1618

1719

1820
def wlen(s: str) -> int:
19-
if len(s) == 1 and s != '\x1a':
21+
if len(s) == 1 and s != "\x1a":
2022
return str_width(s)
2123
length = sum(str_width(i) for i in s)
2224
# remove lengths of any escape sequences
2325
sequence = ANSI_ESCAPE_SEQUENCE.findall(s)
24-
ctrl_z_cnt = s.count('\x1a')
26+
ctrl_z_cnt = s.count("\x1a")
2527
return length - sum(len(i) for i in sequence) + ctrl_z_cnt
28+
29+
30+
def unbracket(s: str, including_content: bool = False) -> str:
31+
r"""Return `s` with \001 and \002 characters removed.
32+
33+
If `including_content` is True, content between \001 and \002 is also
34+
stripped.
35+
"""
36+
if including_content:
37+
return ZERO_WIDTH_BRACKET.sub("", s)
38+
return s.translate(ZERO_WIDTH_TRANS)

Diff for: Lib/test/test_pyrepl/support.py

+24-7
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,24 @@
66
from _pyrepl.console import Console, Event
77
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
88
from _pyrepl.simple_interact import _strip_final_indent
9+
from _pyrepl.utils import unbracket, ANSI_ESCAPE_SEQUENCE
10+
11+
12+
class ScreenEqualMixin:
13+
def assert_screen_equal(
14+
self, reader: ReadlineAlikeReader, expected: str, clean: bool = False
15+
):
16+
actual = clean_screen(reader) if clean else reader.screen
17+
expected = expected.split("\n")
18+
self.assertListEqual(actual, expected)
919

1020

1121
def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None):
1222
saved = reader.more_lines
1323
try:
1424
reader.more_lines = partial(more_lines, namespace=namespace)
15-
reader.ps1 = reader.ps2 = ">>>"
16-
reader.ps3 = reader.ps4 = "..."
25+
reader.ps1 = reader.ps2 = ">>> "
26+
reader.ps3 = reader.ps4 = "... "
1727
return reader.readline()
1828
finally:
1929
reader.more_lines = saved
@@ -38,18 +48,22 @@ def code_to_events(code: str):
3848
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
3949

4050

41-
def clean_screen(screen: Iterable[str]):
51+
def clean_screen(reader: ReadlineAlikeReader) -> list[str]:
4252
"""Cleans color and console characters out of a screen output.
4353
4454
This is useful for screen testing, it increases the test readability since
4555
it strips out all the unreadable side of the screen.
4656
"""
4757
output = []
48-
for line in screen:
49-
if line.startswith(">>>") or line.startswith("..."):
50-
line = line[3:]
58+
for line in reader.screen:
59+
line = unbracket(line, including_content=True)
60+
line = ANSI_ESCAPE_SEQUENCE.sub("", line)
61+
for prefix in (reader.ps1, reader.ps2, reader.ps3, reader.ps4):
62+
if line.startswith(prefix):
63+
line = line[len(prefix):]
64+
break
5165
output.append(line)
52-
return "\n".join(output).strip()
66+
return output
5367

5468

5569
def prepare_reader(console: Console, **kwargs):
@@ -99,6 +113,9 @@ def handle_all_events(
99113
prepare_console=partial(prepare_console, width=10),
100114
)
101115

116+
reader_no_colors = partial(prepare_reader, can_colorize=False)
117+
reader_force_colors = partial(prepare_reader, can_colorize=True)
118+
102119

103120
class FakeConsole(Console):
104121
def __init__(self, events, encoding="utf-8") -> None:

0 commit comments

Comments
 (0)