gh-131507: Add support for syntax highlighting in PyREPL (GH-133247)

Co-authored-by: Victorien <65306057+Viicos@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
This commit is contained in:
Łukasz Langa 2025-05-02 20:22:31 +02:00 committed by GitHub
parent bfcbb28223
commit fac41f56d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 654 additions and 99 deletions

View file

@ -1,14 +1,21 @@
import itertools
import functools
import rlcompleter
from textwrap import dedent
from unittest import TestCase
from unittest.mock import MagicMock
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 .support import prepare_console, reader_force_colors
from .support import reader_no_colors as prepare_reader
from _pyrepl.console import Event
from _pyrepl.reader import Reader
from _colorize import theme
overrides = {"RESET": "z", "SOFT_KEYWORD": "K"}
colors = {overrides.get(k, k[0].lower()): v for k, v in theme.items()}
class TestReader(ScreenEqualMixin, TestCase):
@ -123,8 +130,9 @@ class TestReader(ScreenEqualMixin, TestCase):
def test_control_characters(self):
code = 'flag = "🏳️‍🌈"'
events = code_to_events(code)
reader, _ = handle_all_events(events)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, 'flag = "🏳️\\u200d🌈"', clean=True)
self.assert_screen_equal(reader, 'flag {o}={z} {s}"🏳️\\u200d🌈"{z}'.format(**colors))
def test_setpos_from_xy_multiple_lines(self):
# fmt: off
@ -355,3 +363,140 @@ class TestReader(ScreenEqualMixin, TestCase):
reader, _ = handle_all_events(events)
reader.setpos_from_xy(8, 0)
self.assertEqual(reader.pos, 7)
def test_syntax_highlighting_basic(self):
code = dedent(
"""\
import re, sys
def funct(case: str = sys.platform) -> None:
match = re.search(
"(me)",
'''
Come on
Come on now
You know that it's time to emerge
''',
)
match case:
case "emscripten": print("on the web")
case "ios" | "android": print("on the phone")
case _: print('arms around', match.group(1))
"""
)
expected = dedent(
"""\
{k}import{z} re{o},{z} sys
{a}{k}def{z} {d}funct{z}{o}({z}case{o}:{z} {b}str{z} {o}={z} sys{o}.{z}platform{o}){z} {o}->{z} {k}None{z}{o}:{z}
match {o}={z} re{o}.{z}search{o}({z}
{s}"(me)"{z}{o},{z}
{s}'''{z}
{s} Come on{z}
{s} Come on now{z}
{s} You know that it's time to emerge{z}
{s} '''{z}{o},{z}
{o}){z}
{K}match{z} case{o}:{z}
{K}case{z} {s}"emscripten"{z}{o}:{z} {b}print{z}{o}({z}{s}"on the web"{z}{o}){z}
{K}case{z} {s}"ios"{z} {o}|{z} {s}"android"{z}{o}:{z} {b}print{z}{o}({z}{s}"on the phone"{z}{o}){z}
{K}case{z} {K}_{z}{o}:{z} {b}print{z}{o}({z}{s}'arms around'{z}{o},{z} match{o}.{z}group{o}({z}{n}1{z}{o}){z}{o}){z}
"""
)
expected_sync = expected.format(a="", **colors)
events = code_to_events(code)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected_sync)
self.assertEqual(reader.pos, 2**7 + 2**8)
self.assertEqual(reader.cxy, (0, 14))
async_msg = "{k}async{z} ".format(**colors)
expected_async = expected.format(a=async_msg, **colors)
more_events = itertools.chain(
code_to_events(code),
[Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))] * 13,
code_to_events("async "),
)
reader, _ = handle_all_events(more_events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, expected_async)
self.assertEqual(reader.pos, 21)
self.assertEqual(reader.cxy, (6, 1))
def test_syntax_highlighting_incomplete_string_first_line(self):
code = dedent(
"""\
def unfinished_function(arg: str = "still typing
"""
)
expected = dedent(
"""\
{k}def{z} {d}unfinished_function{z}{o}({z}arg{o}:{z} {b}str{z} {o}={z} {s}"still typing{z}
"""
).format(**colors)
events = code_to_events(code)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
def test_syntax_highlighting_incomplete_string_another_line(self):
code = dedent(
"""\
def unfinished_function(
arg: str = "still typing
"""
)
expected = dedent(
"""\
{k}def{z} {d}unfinished_function{z}{o}({z}
arg{o}:{z} {b}str{z} {o}={z} {s}"still typing{z}
"""
).format(**colors)
events = code_to_events(code)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
def test_syntax_highlighting_incomplete_multiline_string(self):
code = dedent(
"""\
def unfinished_function():
'''Still writing
the docstring
"""
)
expected = dedent(
"""\
{k}def{z} {d}unfinished_function{z}{o}({z}{o}){z}{o}:{z}
{s}'''Still writing{z}
{s} the docstring{z}
"""
).format(**colors)
events = code_to_events(code)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)
def test_syntax_highlighting_incomplete_fstring(self):
code = dedent(
"""\
def unfinished_function():
var = f"Single-quote but {
1
+
1
} multi-line!
"""
)
expected = dedent(
"""\
{k}def{z} {d}unfinished_function{z}{o}({z}{o}){z}{o}:{z}
var {o}={z} {s}f"{z}{s}Single-quote but {z}{o}{OB}{z}
{n}1{z}
{o}+{z}
{n}1{z}
{o}{CB}{z}{s} multi-line!{z}
"""
).format(OB="{", CB="}", **colors)
events = code_to_events(code)
reader, _ = handle_all_events(events, prepare_reader=reader_force_colors)
self.assert_screen_equal(reader, code, clean=True)
self.assert_screen_equal(reader, expected)