Files
RustPython/Lib/test/test_pyrepl/test_interact.py
T
Jeong, YunWon a1ba6a662c Align compiler and AST behavior with CPython (#8138)
* Align compiler and AST behavior with CPython

Update the compiler pipeline, AST module support, and related VM compile/eval paths toward CPython 3.14 behavior.

Use upstream Ruff parser crates directly and keep RustPython-specific syntax preflight handling in the compile path.

Refresh opcode metadata, snapshots, and targeted tests for the aligned bytecode and AST behavior.

* fix

* Fix PR 8138 CI failures

* Fix match mapping wildcard scan complexity

* fixes

* Use Ruff runtime AST fields

* Minimize residual patch diff

* refactor error handling

* Remove _ast conversion context wrappers

* Clean up _ast conversion glue and compiler warning escalation

Rename the Node conversion parameter `ctx` to `vm` throughout the _ast
module.

Remove the unused compile_program, compile_program_single,
compile_block_expression, and compile_expression forwarders; compile_top
stays as the entry point.

Stash an escalated compiler SyntaxWarning so a non-SyntaxWarning category
propagates unchanged instead of being rewritten to SyntaxError, matching
PyErr_ExceptionMatches(SyntaxWarning) in compiler_warn. Drop the now-unused
CompileWarningError::into_codegen_error.

Replace the PositionalArguments two-variant enum with a struct holding a
shared range and a PositionalArgumentsKind enum.

Assisted-by: Claude

* Represent compiler flags as a CompilerFlags bitflags

Replace the loose PY_CF_*/CO_* i32 constants in compile_mode with a
CompilerFlags bitflags covering the PyCF_* flags and __future__
CO_FUTURE_* bits. The Python-visible ast.PyCF_* attribute values are
derived from it via bits(); parser input modes stay plain consts.

builtins::compile and the compile_string flag handling test flags
through CompilerFlags::contains instead of raw bit math.

Assisted-by: Claude

* Fix PR 8138 test regressions

* Fix PR 8138 CI failures

* Avoid shell interpolation in Linux deps action

* Fix review findings in compiler/AST changes

- frame.rs: compute the MATCH_CLASS class name lazily so a successful
  match no longer takes the type name lock or allocates; the
  __match_args__-not-a-tuple branch reuses it instead of recomputing.
- _ast/constant.rs: add the missing leading space to the with_recursion
  label ("maximum recursion depth exceeded during compilation").
- sys.set_int_max_str_digits: restore the "maxdigits must be 0 or larger
  than {n}" error text.
- codegen: set CO_MAXBLOCKS to 20 in both ir.rs and compile.rs.
- _ast/exception.rs: extract the duplicated excepthandler node guard
  into ensure_excepthandler_node.
- codegen: correct the validate_keywords doc comment.
- Drop redundant "CPython" qualifiers from comments added in this branch.

Assisted-by: Claude
2026-06-26 18:22:08 +09:00

304 lines
9.8 KiB
Python
Vendored

import contextlib
import io
import warnings
import unittest
from unittest.mock import patch
from textwrap import dedent
from test.support import force_not_colorized
from _pyrepl.console import InteractiveColoredConsole
from _pyrepl.simple_interact import _more_lines
class TestSimpleInteract(unittest.TestCase):
def test_multiple_statements(self):
namespace = {}
code = dedent("""\
class A:
def foo(self):
pass
class B:
def bar(self):
pass
a = 1
a
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with (
patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror,
patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource,
contextlib.redirect_stdout(f),
):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
showsyntaxerror.assert_not_called()
def test_multiple_statements_output(self):
namespace = {}
code = dedent("""\
b = 1
b
a = 1
a
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with contextlib.redirect_stdout(f):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
self.assertEqual(f.getvalue(), "1\n")
@force_not_colorized
def test_multiple_statements_fail_early(self):
console = InteractiveColoredConsole()
code = dedent("""\
raise Exception('foobar')
print('spam', 'eggs', sep='&')
""")
f = io.StringIO()
with contextlib.redirect_stderr(f):
console.runsource(code)
self.assertIn('Exception: foobar', f.getvalue())
self.assertNotIn('spam&eggs', f.getvalue())
def test_empty(self):
namespace = {}
code = ""
console = InteractiveColoredConsole(namespace, filename="<stdin>")
f = io.StringIO()
with contextlib.redirect_stdout(f):
more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg]
self.assertFalse(more)
self.assertEqual(f.getvalue(), "")
def test_runsource_compiles_and_runs_code(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!')"
with patch.object(console, "runcode") as mock_runcode:
console.runsource(source)
mock_runcode.assert_called_once()
def test_runsource_returns_false_for_successful_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!')"
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
@force_not_colorized
def test_runsource_returns_false_for_failed_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!'"
f = io.StringIO()
with contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertIn('SyntaxError', f.getvalue())
@unittest.expectedFailure # TODO: RUSTPYTHON
@force_not_colorized
def test_runsource_show_syntax_error_location(self):
console = InteractiveColoredConsole()
source = "def f(x, x): ..."
f = io.StringIO()
with contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
r = """
def f(x, x): ...
^
SyntaxError: duplicate argument 'x' in function definition"""
self.assertIn(r, f.getvalue())
def test_runsource_shows_syntax_error_for_failed_compilation(self):
console = InteractiveColoredConsole()
source = "print('Hello, world!'"
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
console.runsource(source)
mock_showsyntaxerror.assert_called_once()
source = dedent("""\
match 1:
case {0: _, 0j: _}:
pass
""")
with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror:
console.runsource(source)
mock_showsyntaxerror.assert_called_once()
def test_runsource_survives_null_bytes(self):
console = InteractiveColoredConsole()
source = "\x00\n"
f = io.StringIO()
with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertIn("source code string cannot contain null bytes", f.getvalue())
def test_no_active_future(self):
console = InteractiveColoredConsole()
source = dedent("""\
x: int = 1
print(__annotate__(1))
""")
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n")
def test_future_annotations(self):
console = InteractiveColoredConsole()
source = dedent("""\
from __future__ import annotations
def g(x: int): ...
print(g.__annotations__)
""")
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource(source)
self.assertFalse(result)
self.assertEqual(f.getvalue(), "{'x': 'int'}\n")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_future_barry_as_flufl(self):
console = InteractiveColoredConsole()
f = io.StringIO()
with contextlib.redirect_stdout(f):
result = console.runsource("from __future__ import barry_as_FLUFL\n")
result = console.runsource("""print("black" <> 'blue')\n""")
self.assertFalse(result)
self.assertEqual(f.getvalue(), "True\n")
class TestMoreLines(unittest.TestCase):
def test_invalid_syntax_single_line(self):
namespace = {}
code = "if foo"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_empty_line(self):
namespace = {}
code = ""
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_valid_single_statement(self):
namespace = {}
code = "foo = 1"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiline_single_assignment(self):
namespace = {}
code = dedent("""\
foo = [
1,
2,
3,
]""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiline_single_block(self):
namespace = {}
code = dedent("""\
def foo():
'''docs'''
return 1""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_statements_single_line(self):
namespace = {}
code = "foo = 1;bar = 2"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiple_statements(self):
namespace = {}
code = dedent("""\
import time
foo = 1""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_blocks(self):
namespace = {}
code = dedent("""\
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
def test_multiple_blocks_empty_newline(self):
namespace = {}
code = dedent("""\
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
""")
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_multiple_blocks_indented_newline(self):
namespace = {}
code = (
"from dataclasses import dataclass\n"
"\n"
"@dataclass\n"
"class Point:\n"
" x: float\n"
" y: float\n"
" "
)
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertFalse(_more_lines(console, code))
def test_incomplete_statement(self):
namespace = {}
code = "if foo:"
console = InteractiveColoredConsole(namespace, filename="<stdin>")
self.assertTrue(_more_lines(console, code))
class TestWarnings(unittest.TestCase):
def test_pep_765_warning(self):
"""
Test that a SyntaxWarning emitted from the
AST optimizer is only shown once in the REPL.
"""
# gh-131927
console = InteractiveColoredConsole()
code = dedent("""\
def f():
try:
return 1
finally:
return 2
""")
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
console.runsource(code)
count = sum("'return' in a 'finally' block" in str(w.message)
for w in caught)
self.assertEqual(count, 1)