mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
Update REPL rendering and completion for Python 3.15 (#9200)
* Preserve pyrepl console mock guards during migration Keep the existing three named-function decorator workarounds and their explanations at exact audited anchors. Reject changed canonical or local preambles, and retain canonical test bodies. Assisted-by: OpenAI Codex:model version unavailable * Update pyrepl rendering and completion for Python 3.15 Sync the complete _pyrepl package and rlcompleter with CPython d220409a13b77de7a6c30abe16ea90db01528edc using scripts/update_lib copy-lib. Migrate the complete test_pyrepl package and existing repl/completion consumers, preserving existing RustPython guards and canonical assertions. This is a source checkpoint. Interpreter, PTY, native platform, workspace, C-API and Clippy validation remain pending; host updater tests and configured commit hooks passed. Assisted-by: OpenAI Codex:model version unavailable * Include lazy soft keyword for canonical REPL highlighting Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
1 parent
fc853015d1
commit
c84d862711
36 files changed
+4705
-663
No files matched your search
@@ -368,6 +368,61 @@ def _preserve_frozen_lock_skip(old, new):
|
||||
)
|
||||
|
||||
|
||||
def _preserve_pyrepl_console_mocks(old: str, new: str) -> str:
|
||||
"""Retain only the three existing decorator-lambda parser workarounds.
|
||||
|
||||
The local preamble is pinned to RustPython e7e3dfd6; the canonical
|
||||
decorators are pinned to CPython d220409a13. Preserve their comments too.
|
||||
Neither class's test bodies participate in this substitution.
|
||||
"""
|
||||
names = (
|
||||
"__rustpython_patch_terminfo_tparm",
|
||||
"__rustpython_patch_termios_tcgetattr",
|
||||
"__rustpython_patch_termios_tcsetattr",
|
||||
)
|
||||
hashes = (
|
||||
"cf1df685289a405fd3022f5790545b7c4218f30c71b0163dda055717c8c91cd8",
|
||||
"63e1ec850e386fba876e14b6c665d4f043a3af77bc4778c53a5897c82375bcc3",
|
||||
)
|
||||
blocks = []
|
||||
for local, contents, expected_hash in zip((True, False), (old, new), hashes):
|
||||
tree = ast.parse(contents)
|
||||
owner = _unique_anchor(
|
||||
contents,
|
||||
tree,
|
||||
lambda node: isinstance(node, ast.ClassDef) and node.name == "TestConsole",
|
||||
direct=True,
|
||||
)
|
||||
if list(_bytes_bindings(tree, "TestConsole")) != [owner]:
|
||||
raise ValueError("Shadowed pyrepl console test class")
|
||||
helpers = []
|
||||
for name in names:
|
||||
bindings = list(_bytes_bindings(tree, name))
|
||||
if local:
|
||||
if (
|
||||
len(bindings) != 1
|
||||
or not isinstance(bindings[0], ast.FunctionDef)
|
||||
or bindings[0] not in tree.body
|
||||
):
|
||||
raise ValueError("Missing or shadowed pyrepl console mock helper")
|
||||
helpers.append(bindings[0])
|
||||
elif bindings:
|
||||
raise ValueError("Canonical pyrepl console mock helper already exists")
|
||||
if not owner.decorator_list:
|
||||
raise ValueError("Missing pyrepl console test decorators")
|
||||
start = helpers[0].lineno if local else owner.decorator_list[0].lineno
|
||||
block = "".join(
|
||||
contents.splitlines(keepends=True)[start - 1 : owner.lineno - 1]
|
||||
)
|
||||
if hashlib.sha256(block.encode("utf-8")).hexdigest() != expected_hash:
|
||||
raise ValueError("Changed pyrepl console mock preamble")
|
||||
blocks.append((start, owner.lineno - 1, block))
|
||||
start, end, _ = blocks[1]
|
||||
lines = new.splitlines(keepends=True)
|
||||
lines[start - 1 : end] = [blocks[0][2]]
|
||||
return "".join(lines)
|
||||
|
||||
|
||||
def preserve_migration_customizations(path: pathlib.Path, old: str, new: str) -> str:
|
||||
"""Fail closed if either known local patch or its canonical anchor changes."""
|
||||
parts = path.parts
|
||||
@@ -413,6 +468,8 @@ def preserve_migration_customizations(path: pathlib.Path, old: str, new: str) ->
|
||||
return _preserve_multiphase_guard(old, new)
|
||||
if parts == ("Lib", "test", "test_importlib", "test_locks.py"):
|
||||
return _preserve_frozen_lock_skip(old, new)
|
||||
if parts == ("Lib", "test", "test_pyrepl", "test_unix_console.py"):
|
||||
return _preserve_pyrepl_console_mocks(old, new)
|
||||
if parts == ("Lib", "test", "test_types.py"):
|
||||
for contents in (old, new):
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
def __rustpython_patch_terminfo_tparm(s, *args):
|
||||
return s + b":" + b",".join(str(i).encode() for i in args)
|
||||
|
||||
|
||||
def __rustpython_patch_termios_tcgetattr(_):
|
||||
return [
|
||||
27394,
|
||||
3,
|
||||
19200,
|
||||
536872399,
|
||||
38400,
|
||||
38400,
|
||||
[
|
||||
b"\x04",
|
||||
b"\xff",
|
||||
b"\xff",
|
||||
b"\x7f",
|
||||
b"\x17",
|
||||
b"\x15",
|
||||
b"\x12",
|
||||
b"\x00",
|
||||
b"\x03",
|
||||
b"\x1c",
|
||||
b"\x1a",
|
||||
b"\x19",
|
||||
b"\x11",
|
||||
b"\x13",
|
||||
b"\x16",
|
||||
b"\x0f",
|
||||
b"\x01",
|
||||
b"\x00",
|
||||
b"\x14",
|
||||
b"\x00",
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def __rustpython_patch_termios_tcsetattr(a, b, c):
|
||||
return None
|
||||
|
||||
@unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows")
|
||||
@patch(
|
||||
"_pyrepl.terminfo.tparm",
|
||||
# TODO: RUSTPYTHON; SyntaxError: the symbol 's' must be present in the symbol table
|
||||
# lambda s, *args: s + b":" + b",".join(str(i).encode() for i in args),
|
||||
__rustpython_patch_terminfo_tparm,
|
||||
)
|
||||
# TODO: RUSTPYTHON; SyntaxError: no symbol table available in lambda (type: Lambda)
|
||||
# @patch(
|
||||
# "termios.tcgetattr",
|
||||
# lambda _: [
|
||||
# 27394,
|
||||
# 3,
|
||||
# 19200,
|
||||
# 536872399,
|
||||
# 38400,
|
||||
# 38400,
|
||||
# [
|
||||
# b"\x04",
|
||||
# b"\xff",
|
||||
# b"\xff",
|
||||
# b"\x7f",
|
||||
# b"\x17",
|
||||
# b"\x15",
|
||||
# b"\x12",
|
||||
# b"\x00",
|
||||
# b"\x03",
|
||||
# b"\x1c",
|
||||
# b"\x1a",
|
||||
# b"\x19",
|
||||
# b"\x11",
|
||||
# b"\x13",
|
||||
# b"\x16",
|
||||
# b"\x0f",
|
||||
# b"\x01",
|
||||
# b"\x00",
|
||||
# b"\x14",
|
||||
# b"\x00",
|
||||
# ],
|
||||
# ],
|
||||
# )
|
||||
@patch("termios.tcgetattr", __rustpython_patch_termios_tcgetattr)
|
||||
@patch(
|
||||
"termios.tcsetattr",
|
||||
# TODO: RUSTPYTHON; SyntaxError: no symbol table available in lambda (type: Lambda)
|
||||
# lambda a, b, c: None
|
||||
__rustpython_patch_termios_tcsetattr,
|
||||
)
|
||||
@patch("os.write")
|
||||
@force_not_colorized_test_class
|
||||
@@ -0,0 +1,41 @@
|
||||
@unittest.skipIf(sys.platform == "win32", "No Unix event queue on Windows")
|
||||
@patch(
|
||||
"_pyrepl.terminfo.tparm",
|
||||
lambda s, *args: s + b":" + b",".join(str(i).encode() for i in args),
|
||||
)
|
||||
@patch(
|
||||
"termios.tcgetattr",
|
||||
lambda _: [
|
||||
27394,
|
||||
3,
|
||||
19200,
|
||||
536872399,
|
||||
38400,
|
||||
38400,
|
||||
[
|
||||
b"\x04",
|
||||
b"\xff",
|
||||
b"\xff",
|
||||
b"\x7f",
|
||||
b"\x17",
|
||||
b"\x15",
|
||||
b"\x12",
|
||||
b"\x00",
|
||||
b"\x03",
|
||||
b"\x1c",
|
||||
b"\x1a",
|
||||
b"\x19",
|
||||
b"\x11",
|
||||
b"\x13",
|
||||
b"\x16",
|
||||
b"\x0f",
|
||||
b"\x01",
|
||||
b"\x00",
|
||||
b"\x14",
|
||||
b"\x00",
|
||||
],
|
||||
],
|
||||
)
|
||||
@patch("termios.tcsetattr", lambda a, b, c: None)
|
||||
@patch("os.write")
|
||||
@force_not_colorized_test_class
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Preserve the existing three pyrepl console mock parser workarounds."""
|
||||
|
||||
import ast
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from update_lib.cmd_migrate import (
|
||||
patch_single_content,
|
||||
preserve_migration_customizations,
|
||||
)
|
||||
from update_lib.patch_spec import extract_patches
|
||||
|
||||
|
||||
class TestPyreplConsoleMigration(unittest.TestCase):
|
||||
path = Path("Lib/test/test_pyrepl/test_unix_console.py")
|
||||
fixtures = Path(__file__).parent / "fixtures"
|
||||
local_preamble = (fixtures / "pyrepl_console_local.txt").read_text()
|
||||
upstream_preamble = (fixtures / "pyrepl_console_upstream.txt").read_text()
|
||||
body = """class TestConsole(TestCase):
|
||||
def test_cursor_back_write(self, _os_write):
|
||||
self.assertEqual(1, 2)
|
||||
|
||||
def test_new_upstream_case(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
class TestUnixConsoleInputHook(TestCase):
|
||||
def test_input_hook_output_is_cooked(self):
|
||||
self.assertEqual(output, b"line1\\r\\nline2\\r\\n")
|
||||
"""
|
||||
prefix = "import unittest\nfrom unittest import TestCase\n"
|
||||
local = prefix + local_preamble + body
|
||||
upstream = prefix + upstream_preamble + body
|
||||
|
||||
def migrate(self, old, new):
|
||||
return preserve_migration_customizations(self.path, old, new)
|
||||
|
||||
def test_preserves_exact_local_preamble_and_canonical_bodies(self):
|
||||
old = self.local.replace("self.assertEqual(1, 2)", "self.assertEqual(1, 1)")
|
||||
migrated = self.migrate(old, self.upstream)
|
||||
self.assertEqual(migrated, self.local)
|
||||
self.assertTrue(migrated.endswith(self.body))
|
||||
self.assertEqual(self.migrate(migrated, self.upstream), migrated)
|
||||
ast.parse(migrated)
|
||||
|
||||
def test_changed_preamble_fails_closed_on_both_sides(self):
|
||||
for local in (True, False):
|
||||
source = self.local if local else self.upstream
|
||||
for changed in (
|
||||
source.replace("27394", "27395", 1),
|
||||
source.replace('"termios.tcgetattr"', '"termios.changed"', 1),
|
||||
source.replace('s + b":"', 's + b";"', 1),
|
||||
source.replace("@patch(\n", "@patch( # changed\n", 1),
|
||||
source.replace("@force_not_colorized_test_class\n", ""),
|
||||
):
|
||||
with self.subTest(local=local, changed=changed):
|
||||
with self.assertRaises(ValueError):
|
||||
self.migrate(
|
||||
changed if local else self.local,
|
||||
self.upstream if local else changed,
|
||||
)
|
||||
|
||||
def test_comment_loss_fails_closed(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.migrate(
|
||||
self.local.replace("TODO: RUSTPYTHON", "TODO", 1), self.upstream
|
||||
)
|
||||
|
||||
def test_missing_duplicate_nested_or_shadowed_owner_fails_closed(self):
|
||||
for local in (True, False):
|
||||
source = self.local if local else self.upstream
|
||||
for changed in (
|
||||
source.replace("class TestConsole(", "class OtherConsole("),
|
||||
source + "\nclass TestConsole: pass\n",
|
||||
"if True:\n"
|
||||
+ "".join(" " + line for line in source.splitlines(True)),
|
||||
source + "\nTestConsole = None\n",
|
||||
):
|
||||
with self.subTest(local=local, changed=changed):
|
||||
with self.assertRaises(ValueError):
|
||||
self.migrate(
|
||||
changed if local else self.local,
|
||||
self.upstream if local else changed,
|
||||
)
|
||||
|
||||
def test_missing_duplicate_or_shadowed_helpers_fail_closed(self):
|
||||
for name in (
|
||||
"__rustpython_patch_terminfo_tparm",
|
||||
"__rustpython_patch_termios_tcgetattr",
|
||||
"__rustpython_patch_termios_tcsetattr",
|
||||
):
|
||||
for changed in (
|
||||
self.local.replace("def " + name, "def missing_helper"),
|
||||
self.local + f"\ndef {name}(): pass\n",
|
||||
self.local + f"\n{name} = None\n",
|
||||
self.local + f"\ndel {name}\n",
|
||||
self.local + f"\nimport other as {name}\n",
|
||||
):
|
||||
with self.subTest(name=name, changed=changed):
|
||||
with self.assertRaises(ValueError):
|
||||
self.migrate(changed, self.upstream)
|
||||
with self.assertRaises(ValueError):
|
||||
self.migrate(self.local, self.upstream + f"\n{name} = None\n")
|
||||
|
||||
def test_other_paths_are_unchanged(self):
|
||||
self.assertEqual(
|
||||
preserve_migration_customizations(
|
||||
Path("Lib/test/test_other.py"), self.local, self.upstream
|
||||
),
|
||||
self.upstream,
|
||||
)
|
||||
|
||||
def test_normal_migration_retains_existing_decorator_guard(self):
|
||||
guard = (
|
||||
' @unittest.expectedFailureIf(sys.platform == "android", '
|
||||
'"TODO: RUSTPYTHON; existing signal handler limitation")\n'
|
||||
)
|
||||
local = self.local.replace(
|
||||
" def test_cursor_back_write", guard + " def test_cursor_back_write"
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
directory = Path(root)
|
||||
source = directory / "canonical.py"
|
||||
target = directory / self.path
|
||||
source.write_text(self.upstream)
|
||||
target.parent.mkdir(parents=True)
|
||||
target.write_text(local)
|
||||
with (
|
||||
patch(
|
||||
"update_lib.cmd_migrate.__file__",
|
||||
directory / "scripts/update_lib/cmd_migrate.py",
|
||||
),
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
result = patch_single_content(source, target)
|
||||
self.assertEqual(extract_patches(result), extract_patches(local))
|
||||
self.assertIn(self.local_preamble, result)
|
||||
self.assertIn(self.body.split("class TestUnixConsoleInputHook")[1], result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user