Skip to content

Commit 095c126

Browse files
authored
[3.13] gh-131507: Clean up tests and type checking for _pyrepl (GH-131509) (GH-131546)
(cherry picked from commit 5d8e981)
1 parent 0a22407 commit 095c126

14 files changed

+233
-132
lines changed

Lib/_colorize.py

+53-8
Original file line numberDiff line numberDiff line change
@@ -1,21 +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-
BOLD_GREEN = "\x1b[1;32m"
10-
BOLD_MAGENTA = "\x1b[1;35m"
11-
BOLD_RED = "\x1b[1;31m"
14+
RESET = "\x1b[0m"
15+
16+
BLACK = "\x1b[30m"
17+
BLUE = "\x1b[34m"
18+
CYAN = "\x1b[36m"
1219
GREEN = "\x1b[32m"
13-
GREY = "\x1b[90m"
1420
MAGENTA = "\x1b[35m"
1521
RED = "\x1b[31m"
16-
RESET = "\x1b[0m"
22+
WHITE = "\x1b[37m" # more like LIGHT GRAY
1723
YELLOW = "\x1b[33m"
1824

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+
1962

2063
NoColors = ANSIColors()
2164

@@ -24,14 +67,16 @@ class ANSIColors:
2467
setattr(NoColors, attr, "")
2568

2669

27-
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:
2873
if colorize or can_colorize(file=file):
2974
return ANSIColors()
3075
else:
3176
return NoColors
3277

3378

34-
def can_colorize(*, file=None) -> bool:
79+
def can_colorize(*, file: IO[str] | IO[bytes] | None = None) -> bool:
3580
if file is None:
3681
file = sys.stdout
3782

@@ -64,4 +109,4 @@ def can_colorize(*, file=None) -> bool:
64109
try:
65110
return os.isatty(file.fileno())
66111
except io.UnsupportedOperation:
67-
return file.isatty()
112+
return hasattr(file, "isatty") and file.isatty()

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()

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
@@ -160,7 +160,7 @@ def __init__(
160160
*,
161161
local_exit: bool = False,
162162
) -> None:
163-
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
163+
super().__init__(locals=locals, filename=filename, local_exit=local_exit)
164164
self.can_colorize = _colorize.can_colorize()
165165

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

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

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

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

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)

Lib/test/test_pyrepl/support.py

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

1121

1222
def multiline_input(reader: ReadlineAlikeReader, namespace: dict | None = None):
1323
saved = reader.more_lines
1424
try:
1525
reader.more_lines = partial(more_lines, namespace=namespace)
16-
reader.ps1 = reader.ps2 = ">>>"
17-
reader.ps3 = reader.ps4 = "..."
26+
reader.ps1 = reader.ps2 = ">>> "
27+
reader.ps3 = reader.ps4 = "... "
1828
return reader.readline()
1929
finally:
2030
reader.more_lines = saved
@@ -39,18 +49,22 @@ def code_to_events(code: str):
3949
yield Event(evt="key", data=c, raw=bytearray(c.encode("utf-8")))
4050

4151

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

5569

5670
def prepare_reader(console: Console, **kwargs):
@@ -100,6 +114,9 @@ def handle_all_events(
100114
prepare_console=partial(prepare_console, width=10),
101115
)
102116

117+
reader_no_colors = partial(prepare_reader, can_colorize=False)
118+
reader_force_colors = partial(prepare_reader, can_colorize=True)
119+
103120

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

0 commit comments

Comments
 (0)