Fix traceback, syntax errors, and exception handling (#7015)

* Update codeop from v3.14.3

* Fix traceback, syntax errors, and exception handling

- Improve unclosed bracket detection with "'(' was never closed" message
- Fix IndentationError location to point to end of line
- Implement frame.clear() with proper checks for executing/suspended frames
- Fix exception context chaining for propagated exceptions
- Add traceback.__dir__() and prevent tb_next deletion
- Fix subscript operation source range restoration in compiler
- Change "duplicate parameter" to "duplicate argument" error message
- Refactor duplicate code in asyncgenerator.rs and frame.rs

---------

Co-authored-by: CPython Developers <>
This commit is contained in:
Jeong, YunWonandCPython Developers authored and GitHub committed 2026-02-07 13:41:45 +09:00
1 parent 234bdda40d
commit 6b870d62ad
29 files changed
+545 -245

No files matched your search

+7 -8
View File
@@ -47,7 +47,7 @@ PyCF_DONT_IMPLY_DEDENT = 0x200
PyCF_ONLY_AST = 0x400
PyCF_ALLOW_INCOMPLETE_INPUT = 0x4000
def _maybe_compile(compiler, source, filename, symbol):
def _maybe_compile(compiler, source, filename, symbol, flags):
# Check for source consisting of only blank lines and comments.
for line in source.split("\n"):
line = line.strip()
@@ -61,10 +61,10 @@ def _maybe_compile(compiler, source, filename, symbol):
with warnings.catch_warnings():
warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning))
try:
compiler(source, filename, symbol)
compiler(source, filename, symbol, flags=flags)
except SyntaxError: # Let other compile() errors propagate.
try:
compiler(source + "\n", filename, symbol)
compiler(source + "\n", filename, symbol, flags=flags)
return None
except _IncompleteInputError as e:
return None
@@ -74,14 +74,13 @@ def _maybe_compile(compiler, source, filename, symbol):
return compiler(source, filename, symbol, incomplete_input=False)
def _compile(source, filename, symbol, incomplete_input=True):
flags = 0
def _compile(source, filename, symbol, incomplete_input=True, *, flags=0):
if incomplete_input:
flags |= PyCF_ALLOW_INCOMPLETE_INPUT
flags |= PyCF_DONT_IMPLY_DEDENT
return compile(source, filename, symbol, flags)
def compile_command(source, filename="<input>", symbol="single"):
def compile_command(source, filename="<input>", symbol="single", flags=0):
r"""Compile a command and determine whether it is incomplete.
Arguments:
@@ -100,7 +99,7 @@ def compile_command(source, filename="<input>", symbol="single"):
syntax error (OverflowError and ValueError can be produced by
malformed literals).
"""
return _maybe_compile(_compile, source, filename, symbol)
return _maybe_compile(_compile, source, filename, symbol, flags)
class Compile:
"""Instances of this class behave much like the built-in compile
@@ -152,4 +151,4 @@ class CommandCompiler:
syntax error (OverflowError and ValueError can be produced by
malformed literals).
"""
return _maybe_compile(self.compiler, source, filename, symbol)
return _maybe_compile(self.compiler, source, filename, symbol, flags=self.compiler.flags)
+2
View File
@@ -2947,6 +2947,7 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest,
return super().test_log_destroyed_pending_task()
@unittest.skipUnless(hasattr(futures, '_CFuture') and
hasattr(tasks, '_CTask'),
'requires the C _asyncio module')
@@ -3007,6 +3008,7 @@ class CTask_PyFuture_Tests(BaseTaskTests, test_utils.TestCase):
return super().test_log_destroyed_pending_task()
@unittest.skipUnless(hasattr(futures, '_CFuture'),
'requires the C _asyncio module')
class PyTask_CFuture_Tests(BaseTaskTests, test_utils.TestCase):
-2
View File
@@ -548,8 +548,6 @@ class CmdLineTest(unittest.TestCase):
self.assertIn(b'Exception in __main__ module', err)
self.assertIn(b'Traceback', err)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_pep_409_verbiage(self):
# Make sure PEP 409 syntax properly suppresses
# the context of an exception
-3
View File
@@ -91,7 +91,6 @@ class TestInteractiveConsole(unittest.TestCase, MockSys):
else:
raise AssertionError("no console stdout")
@unittest.expectedFailure # TODO: RUSTPYTHON; + 'SyntaxError: invalid syntax']
def test_syntax_error(self):
self.infunc.side_effect = ["def f():",
" x = ?",
@@ -166,7 +165,6 @@ class TestInteractiveConsole(unittest.TestCase, MockSys):
' File "<console>", line 2, in f\n',
'ValueError: BOOM!\n'])
@unittest.expectedFailure # TODO: RUSTPYTHON; + 'SyntaxError: invalid syntax\n']
def test_sysexcepthook_syntax_error(self):
self.infunc.side_effect = ["def f():",
" x = ?",
@@ -285,7 +283,6 @@ class TestInteractiveConsole(unittest.TestCase, MockSys):
self.assertEqual(err_msg, ['write', (expected,), {}])
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: '\nAttributeError\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File "<console>", line 1, in <module>\nValueError\n' not found in 'Python <MagicMock name=\'sys.version\' id=\'94615517503920\'> on <MagicMock name=\'sys.platform\' id=\'94615517656384\'>\nType "help", "copyright", "credits" or "license" for more information.\n(InteractiveConsole)\nAttributeError\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File "<console>", line 1, in <module>\nValueError: \n\nnow exiting InteractiveConsole...\n'
def test_cause_tb(self):
self.infunc.side_effect = ["raise ValueError('') from AttributeError",
EOFError('Finished')]
+5 -11
View File
@@ -30,8 +30,7 @@ class CodeopTests(unittest.TestCase):
except OverflowError:
self.assertTrue(not is_syntax)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <code object <module> at 0xc99532080 file "<input>", line 1> != <code object <module> at 0xc99532f80 file "<input>", line 1>
def test_valid(self):
av = self.assertValid
@@ -94,8 +93,7 @@ class CodeopTests(unittest.TestCase):
av("def f():\n pass\n#foo\n")
av("@a.b.c\ndef f():\n pass\n")
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <code object <module> at 0xc99532080 file "<input>", line 1> != None
def test_incomplete(self):
ai = self.assertIncomplete
@@ -282,13 +280,12 @@ class CodeopTests(unittest.TestCase):
self.assertNotEqual(compile_command("a = 1\n", "abc").co_filename,
compile("a = 1\n", "def", 'single').co_filename)
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 2
def test_warning(self):
# Test that the warning is only returned once.
with warnings_helper.check_warnings(
('"is" with \'str\' literal', SyntaxWarning),
("invalid escape sequence", SyntaxWarning),
('"\\\\e" is an invalid escape sequence', SyntaxWarning),
) as w:
compile_command(r"'\e' is 0")
self.assertEqual(len(w.warnings), 2)
@@ -309,8 +306,7 @@ class CodeopTests(unittest.TestCase):
self.assertIncomplete("'\\e' + (")
self.assertEqual(w, [])
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 1
def test_invalid_warning(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
@@ -325,8 +321,6 @@ class CodeopTests(unittest.TestCase):
with self.assertRaisesRegex(SyntaxError, message):
compile_command(code, symbol='exec')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_syntax_errors(self):
self.assertSyntaxErrorMatches(
dedent("""\
-6
View File
@@ -924,8 +924,6 @@ class TestBaseExitStack:
self.assertIsInstance(inner_exc, ValueError)
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_exit_exception_chaining(self):
# Ensure exception chaining matches the reference behaviour
def raise_exc(exc):
@@ -957,8 +955,6 @@ class TestBaseExitStack:
self.assertIsInstance(inner_exc, ValueError)
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_exit_exception_explicit_none_context(self):
# Ensure ExitStack chaining matches actual nested `with` statements
# regarding explicit __context__ = None.
@@ -1053,8 +1049,6 @@ class TestBaseExitStack:
self.assertIsNone(
exc.__context__.__context__.__context__.__context__)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_exit_exception_with_existing_context(self):
# Addresses a lack of test coverage discovered after checking in a
# fix for issue 20317 that still contained debugging code.
-2
View File
@@ -650,7 +650,6 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.IsolatedAsyncioTestCase):
await stack.enter_async_context(LacksExit())
self.assertFalse(stack._exit_callbacks)
@unittest.expectedFailure # TODO: RUSTPYTHON
async def test_async_exit_exception_chaining(self):
# Ensure exception chaining matches the reference behaviour
async def raise_exc(exc):
@@ -682,7 +681,6 @@ class TestAsyncExitStack(TestBaseExitStack, unittest.IsolatedAsyncioTestCase):
self.assertIsInstance(inner_exc, ValueError)
self.assertIsInstance(inner_exc.__context__, ZeroDivisionError)
@unittest.expectedFailure # TODO: RUSTPYTHON
async def test_async_exit_exception_explicit_none_context(self):
# Ensure AsyncExitStack chaining matches actual nested `with` statements
# regarding explicit __context__ = None.
-2
View File
@@ -1745,7 +1745,6 @@ class ExceptionTests(unittest.TestCase):
f"deallocator {obj_repr}")
self.assertIsNotNone(cm.unraisable.exc_traceback)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_unhandled(self):
# Check for sensible reporting of unhandled exceptions
for exc_type in (ValueError, BrokenStrException):
@@ -2283,7 +2282,6 @@ class AssertionErrorTests(unittest.TestCase):
class SyntaxErrorTests(unittest.TestCase):
maxDiff = None
@unittest.expectedFailure # TODO: RUSTPYTHON
@force_not_colorized
def test_range_of_offsets(self):
cases = [
+2
View File
@@ -188,6 +188,8 @@ class FutureTest(unittest.TestCase):
exec("from __future__ import unicode_literals; x = ''", {}, scope)
self.assertIsInstance(scope["x"], str)
# TODO: RUSTPYTHON; barry_as_FLUFL (<> operator) not supported
@unittest.expectedFailure
def test_syntactical_future_repl(self):
p = spawn_python('-i')
p.stdin.write(b"from __future__ import barry_as_FLUFL\n")
-1
View File
@@ -221,7 +221,6 @@ the \'lazy\' dog.\n\
self.assertTrue(x is Ellipsis)
self.assertRaises(SyntaxError, eval, ".. .")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_eof_error(self):
samples = ("def foo(", "\ndef foo(", "def foo(\n")
for s in samples:
-1
View File
@@ -568,7 +568,6 @@ class TestInterpreterStack(IsTestBase):
self.assertIn('inspect.stack()', record.code_context[0])
self.assertEqual(record.index, 0)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_trace(self):
self.assertEqual(len(git.tr), 3)
frame1, frame2, frame3, = git.tr
-12
View File
@@ -44,32 +44,24 @@ class NamedExpressionInvalidTest(unittest.TestCase):
with self.assertRaisesRegex(SyntaxError, "cannot use assignment expressions with tuple"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_07(self):
code = """def spam(a = b := 42): pass"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_08(self):
code = """def spam(a: b := 42 = 5): pass"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_09(self):
code = """spam(a=b := 'c')"""
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_10(self):
code = """spam(x = y := f(x))"""
@@ -103,8 +95,6 @@ class NamedExpressionInvalidTest(unittest.TestCase):
"positional argument follows keyword argument"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_14(self):
code = """(x := lambda: y := 1)"""
@@ -120,8 +110,6 @@ class NamedExpressionInvalidTest(unittest.TestCase):
"cannot use assignment expressions with lambda"):
exec(code, {}, {})
# TODO: RUSTPYTHON: wrong error message
@unittest.expectedFailure
def test_named_expression_invalid_16(self):
code = "[i + 1 for i in i := [1,2]]"
-1
View File
@@ -2293,7 +2293,6 @@ class SyntaxTestCase(unittest.TestCase):
offset=7
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_curly_brace_after_primary_raises_immediately(self):
self._check_error("f{}", "invalid syntax", mode="single")
-1
View File
@@ -176,7 +176,6 @@ class ExceptHookTest(unittest.TestCase):
self.assertRaises(TypeError, sys.__excepthook__)
@unittest.expectedFailure # TODO: RUSTPYTHON; SyntaxError formatting in arbitrary tracebacks
@force_not_colorized
def test_excepthook_bytes_filename(self):
# bpo-37467: sys.excepthook() must not crash if a filename
-27
View File
@@ -88,7 +88,6 @@ class TracebackCases(unittest.TestCase):
def tokenizer_error_with_caret_range(self):
compile("blech ( ", "?", "exec")
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 11 != 14
def test_caret(self):
err = self.get_exception_format(self.syntax_error_with_caret,
SyntaxError)
@@ -201,7 +200,6 @@ class TracebackCases(unittest.TestCase):
finally:
unlink(TESTFN)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 3 != 4
def test_bad_indentation(self):
err = self.get_exception_format(self.syntax_error_bad_indentation,
IndentationError)
@@ -1797,7 +1795,6 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
("for x im n:\n pass", "in"),
]
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_keyword_suggestions_from_file(self):
with tempfile.TemporaryDirectory() as script_dir:
for i, (code, expected_kw) in enumerate(self.TYPO_CASES):
@@ -1808,7 +1805,6 @@ class TestKeywordTypoSuggestions(unittest.TestCase):
stderr_text = stderr.decode('utf-8')
self.assertIn(f"Did you mean '{expected_kw}'", stderr_text)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_keyword_suggestions_from_command_string(self):
for code, expected_kw in self.TYPO_CASES:
with self.subTest(typo=expected_kw):
@@ -3352,7 +3348,6 @@ class MiscTracebackCases(unittest.TestCase):
# Check non-printing functions in traceback module
#
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 1 != 0
def test_clear(self):
def outer():
middle()
@@ -3574,7 +3569,6 @@ class TestStack(unittest.TestCase):
f' File "{__file__}", line {lno}, in f\n 1/0\n'
)
@unittest.expectedFailure # TODO: RUSTPYTHON; Actual: _should_show_carets(13, 14, ['# this line will be used during rendering'], None)
def test_summary_should_show_carets(self):
# See: https://github.com/python/cpython/issues/122353
@@ -3731,7 +3725,6 @@ class TestTracebackException(unittest.TestCase):
self.assertEqual(type(exc_obj).__name__, exc.exc_type_str)
self.assertEqual(str(exc_obj), str(exc))
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 11 not greater than 1000
def test_long_context_chain(self):
def f():
try:
@@ -4059,7 +4052,6 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
self.assertEqual(formatted, expected)
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 2265 characters long. Set self.maxDiff to None to see it.
def test_exception_group_format(self):
teg = traceback.TracebackException.from_exception(self.eg)
@@ -4841,22 +4833,6 @@ class PurePythonSuggestionFormattingTests(
traceback printing in traceback.py.
"""
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluch'" not found in "ImportError: cannot import name 'blach'"
def test_import_from_suggestions_underscored(self):
return super().test_import_from_suggestions_underscored()
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluch'" not found in "ImportError: cannot import name 'blech'"
def test_import_from_suggestions_non_string(self):
return super().test_import_from_suggestions_non_string()
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "'bluchin'?" not found in "ImportError: cannot import name 'bluch'"
def test_import_from_suggestions(self):
return super().test_import_from_suggestions()
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'Did you mean' not found in "AttributeError: 'A' object has no attribute 'blich'"
def test_attribute_error_inside_nested_getattr(self):
return super().test_attribute_error_inside_nested_getattr()
@cpython_only
class CPythonSuggestionFormattingTests(
@@ -4969,7 +4945,6 @@ class MiscTest(unittest.TestCase):
class TestColorizedTraceback(unittest.TestCase):
maxDiff = None
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: "y = \x1b[31mx['a']['b']\x1b[0m\x1b[1;31m['c']\x1b[0m" not found in 'Traceback (most recent call last):\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4764\x1b[0m, in \x1b[35mtest_colorized_traceback\x1b[0m\n \x1b[31mbar\x1b[0m\x1b[1;31m()\x1b[0m\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^\x1b[0m\n bar = <function TestColorizedTraceback.test_colorized_traceback.<locals>.bar at 0xb57b09180>\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n e = TypeError("\'NoneType\' object is not subscriptable")\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n self = <test.test_traceback.TestColorizedTraceback testMethod=test_colorized_traceback>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4760\x1b[0m, in \x1b[35mbar\x1b[0m\n return baz1(1,\n 2,3\n ,4)\n baz1 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz1 at 0xb57b09e00>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4757\x1b[0m, in \x1b[35mbaz1\x1b[0m\n return baz2(1,2,3,4)\n args = (1, 2, 3, 4)\n baz2 = <function TestColorizedTraceback.test_colorized_traceback.<locals>.baz2 at 0xb57b09cc0>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35mbaz2\x1b[0m\n return \x1b[31m(lambda *args: foo(*args))\x1b[0m\x1b[1;31m(1,2,3,4)\x1b[0m\n \x1b[31m~~~~~~~~~~~~~~~~~~~~~~~~~~\x1b[0m\x1b[1;31m^^^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4754\x1b[0m, in \x1b[35m<lambda>\x1b[0m\n return (lambda *args: \x1b[31mfoo\x1b[0m\x1b[1;31m(*args)\x1b[0m)(1,2,3,4)\n \x1b[31m~~~\x1b[0m\x1b[1;31m^^^^^^^\x1b[0m\n args = (1, 2, 3, 4)\n foo = <function TestColorizedTraceback.test_colorized_traceback.<locals>.foo at 0xb57b08140>\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4751\x1b[0m, in \x1b[35mfoo\x1b[0m\n y = x[\'a\'][\'b\'][\x1b[1;31m\'c\'\x1b[0m]\n \x1b[1;31m^^^\x1b[0m\n args = (1, 2, 3, 4)\n x = {\'a\': {\'b\': None}}\n\x1b[1;35mTypeError\x1b[0m: \x1b[35m\'NoneType\' object is not subscriptable\x1b[0m\n'
def test_colorized_traceback(self):
def foo(*args):
x = {'a':{'b': None}}
@@ -5002,7 +4977,6 @@ class TestColorizedTraceback(unittest.TestCase):
self.assertIn("return baz1(1,\n 2,3\n ,4)", lines)
self.assertIn(red + "bar" + reset + boldr + "()" + reset, lines)
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ' File \x1b[35m"<string>"\x1b[0m, line \x1b[35m1\x1b[0m\n a \x1b[1;31m$\x1b[0m b\n \x1b[1;31m^\x1b[0m\n\x1b[1;35mSyntaxError\x1b[0m: \x1b[35minvalid syntax\x1b[0m\n' not found in 'Traceback (most recent call last):\n File \x1b[35m"/Users/al03219714/Projects/RustPython/crates/pylib/Lib/test/test_traceback.py"\x1b[0m, line \x1b[35m4782\x1b[0m, in \x1b[35mtest_colorized_syntax_error\x1b[0m\n \x1b[31mcompile\x1b[0m\x1b[1;31m("a $ b", "<string>", "exec")\x1b[0m\n \x1b[31m~~~~~~~\x1b[0m\x1b[1;31m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1b[0m\n e = SyntaxError(\'got unexpected token $\')\n self = <test.test_traceback.TestColorizedTraceback testMethod=test_colorized_syntax_error>\n File \x1b[35m"<string>"\x1b[0m, line \x1b[35m1\x1b[0m\n a \x1b[1;31m$\x1b[0m b\n \x1b[1;31m^\x1b[0m\n\x1b[1;35mSyntaxError\x1b[0m: \x1b[35mgot unexpected token $\x1b[0m\n'
def test_colorized_syntax_error(self):
try:
compile("a $ b", "<string>", "exec")
@@ -5053,7 +5027,6 @@ class TestColorizedTraceback(unittest.TestCase):
]
self.assertEqual(actual, expected(**colors))
@unittest.expectedFailure # TODO: RUSTPYTHON; Diff is 1795 characters long. Set self.maxDiff to None to see it.
def test_colorized_traceback_from_exception_group(self):
def foo():
exceptions = []
+1 -1
View File
@@ -183,7 +183,7 @@ List comprehension element unpacking
# ^
# SyntaxError: invalid syntax
>>> dict(**x for x in [{1:2}]) # TODO: RUSTPYTHON # doctest:+EXPECTED_FAILURE
>>> dict(**x for x in [{1:2}])
Traceback (most recent call last):
...
dict(**x for x in [{1:2}])
+28 -36
View File
@@ -493,56 +493,46 @@ impl Compiler {
slice: &ast::Expr,
ctx: ast::ExprContext,
) -> CompileResult<()> {
// 1. Check subscripter and index for Load context
// 2. VISIT value
// 3. Handle two-element slice specially
// 4. Otherwise VISIT slice and emit appropriate instruction
// For Load context, some checks are skipped for now
// if ctx == ast::ExprContext::Load {
// check_subscripter(value);
// check_index(value, slice);
// }
// Save full subscript expression range (set by compile_expression before this call)
let subscript_range = self.current_source_range;
// VISIT(c, expr, e->v.Subscript.value)
self.compile_expression(value)?;
// Handle two-element non-constant slice with BINARY_SLICE/STORE_SLICE
if slice.should_use_slice_optimization() && !matches!(ctx, ast::ExprContext::Del) {
let use_slice_opt = matches!(ctx, ast::ExprContext::Load | ast::ExprContext::Store)
&& slice.should_use_slice_optimization();
if use_slice_opt {
match slice {
ast::Expr::Slice(s) => self.compile_slice_two_parts(s)?,
_ => unreachable!(
"should_use_slice_optimization should only return true for ast::Expr::Slice"
),
};
match ctx {
ast::ExprContext::Load => {
emit!(self, Instruction::BinarySlice);
}
ast::ExprContext::Store => {
emit!(self, Instruction::StoreSlice);
}
_ => unreachable!(),
}
} else {
// VISIT(c, expr, e->v.Subscript.slice)
self.compile_expression(slice)?;
}
// Emit appropriate instruction based on context
match ctx {
ast::ExprContext::Load => emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subscr
}
),
ast::ExprContext::Store => emit!(self, Instruction::StoreSubscr),
ast::ExprContext::Del => emit!(self, Instruction::DeleteSubscr),
ast::ExprContext::Invalid => {
return Err(self.error(CodegenErrorType::SyntaxError(
"Invalid expression context".to_owned(),
)));
// Restore full subscript expression range before emitting
self.set_source_range(subscript_range);
match (use_slice_opt, ctx) {
(true, ast::ExprContext::Load) => emit!(self, Instruction::BinarySlice),
(true, ast::ExprContext::Store) => emit!(self, Instruction::StoreSlice),
(true, _) => unreachable!(),
(false, ast::ExprContext::Load) => emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subscr
}
),
(false, ast::ExprContext::Store) => emit!(self, Instruction::StoreSubscr),
(false, ast::ExprContext::Del) => emit!(self, Instruction::DeleteSubscr),
(false, ast::ExprContext::Invalid) => {
return Err(self.error(CodegenErrorType::SyntaxError(
"Invalid expression context".to_owned(),
)));
}
}
@@ -6603,7 +6593,8 @@ impl Compiler {
self.compile_expression(left)?;
self.compile_expression(right)?;
// Perform operation:
// Restore full expression range before emitting the operation
self.set_source_range(range);
self.compile_op(op, false);
}
ast::Expr::Subscript(ast::ExprSubscript {
@@ -6614,7 +6605,8 @@ impl Compiler {
ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => {
self.compile_expression(operand)?;
// Perform operation:
// Restore full expression range before emitting the operation
self.set_source_range(range);
match op {
ast::UnaryOp::UAdd => emit!(
self,
+1 -1
View File
@@ -1012,7 +1012,7 @@ impl SymbolTableBuilder {
if table.symbols.contains_key(parameter.name.as_str()) {
return Err(SymbolTableError {
error: format!(
"duplicate parameter '{}' in function definition",
"duplicate argument '{}' in function definition",
parameter.name
),
location: Some(
+160 -13
View File
@@ -27,6 +27,8 @@ pub struct ParseError {
pub location: SourceLocation,
pub end_location: SourceLocation,
pub source_path: String,
/// Set when the error is an unclosed bracket (converted from EOF).
pub is_unclosed_bracket: bool,
}
impl ::core::fmt::Display for ParseError {
@@ -46,26 +48,71 @@ pub enum CompileError {
impl CompileError {
pub fn from_ruff_parse_error(error: parser::ParseError, source_file: &SourceFile) -> Self {
let source_code = source_file.to_source_code();
let location = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_location =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
let source_text = source_file.source_text();
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_location.character_offset.get() == 1 && end_location.line > location.line {
// Get the end of the previous line
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_location = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
// Adjust column to be after the last character
end_location.character_offset = end_location.character_offset.saturating_add(1);
}
// For EOF errors (unclosed brackets), find the unclosed bracket position
// and adjust both the error location and message
let mut is_unclosed_bracket = false;
let (error_type, location, end_location) = if matches!(
&error.error,
parser::ParseErrorType::Lexical(parser::LexicalErrorType::Eof)
) {
if let Some((bracket_char, bracket_offset)) = find_unclosed_bracket(source_text) {
let bracket_text_size = ruff_text_size::TextSize::new(bracket_offset as u32);
let loc = source_code.source_location(bracket_text_size, PositionEncoding::Utf8);
let end_loc = SourceLocation {
line: loc.line,
character_offset: loc.character_offset.saturating_add(1),
};
let msg = format!("'{}' was never closed", bracket_char);
is_unclosed_bracket = true;
(parser::ParseErrorType::OtherError(msg), loc, end_loc)
} else {
let loc =
source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
(error.error, loc, end_loc)
}
} else if matches!(
&error.error,
parser::ParseErrorType::Lexical(parser::LexicalErrorType::IndentationError)
) {
// For IndentationError, point the offset to the end of the line content
// instead of the beginning
let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let line_idx = loc.line.to_zero_indexed();
let line = source_text.split('\n').nth(line_idx).unwrap_or("");
let line_end_col = line.chars().count() + 1; // 1-indexed, past last char
let end_loc = SourceLocation {
line: loc.line,
character_offset: ruff_source_file::OneIndexed::new(line_end_col)
.unwrap_or(loc.character_offset),
};
(error.error, end_loc, end_loc)
} else {
let loc = source_code.source_location(error.location.start(), PositionEncoding::Utf8);
let mut end_loc =
source_code.source_location(error.location.end(), PositionEncoding::Utf8);
// If the error range ends at the start of a new line (column 1),
// adjust it to the end of the previous line
if end_loc.character_offset.get() == 1 && end_loc.line > loc.line {
let prev_line_end = error.location.end() - ruff_text_size::TextSize::from(1);
end_loc = source_code.source_location(prev_line_end, PositionEncoding::Utf8);
end_loc.character_offset = end_loc.character_offset.saturating_add(1);
}
(error.error, loc, end_loc)
};
Self::Parse(ParseError {
error: error.error,
error: error_type,
raw_location: error.location,
location,
end_location,
source_path: source_file.name().to_owned(),
is_unclosed_bracket,
})
}
@@ -102,6 +149,106 @@ impl CompileError {
}
}
/// Find the last unclosed opening bracket in source code.
/// Returns the bracket character and its byte offset, or None if all brackets are balanced.
fn find_unclosed_bracket(source: &str) -> Option<(char, usize)> {
let mut stack: Vec<(char, usize)> = Vec::new();
let mut in_string = false;
let mut string_quote = '\0';
let mut triple_quote = false;
let mut escape_next = false;
let mut is_raw_string = false;
let chars: Vec<(usize, char)> = source.char_indices().collect();
let mut i = 0;
while i < chars.len() {
let (byte_offset, ch) = chars[i];
if escape_next {
escape_next = false;
i += 1;
continue;
}
if in_string {
if ch == '\\' && !is_raw_string {
escape_next = true;
} else if triple_quote {
if ch == string_quote
&& i + 2 < chars.len()
&& chars[i + 1].1 == string_quote
&& chars[i + 2].1 == string_quote
{
in_string = false;
i += 3;
continue;
}
} else if ch == string_quote {
in_string = false;
}
i += 1;
continue;
}
// Check for comments
if ch == '#' {
// Skip to end of line
while i < chars.len() && chars[i].1 != '\n' {
i += 1;
}
continue;
}
// Check for string start (with optional prefix like r, b, f, u, rb, br, etc.)
if ch == '\'' || ch == '"' {
// Check up to 2 characters before the quote for string prefix
is_raw_string = false;
for look_back in 1..=2.min(i) {
let prev = chars[i - look_back].1;
if matches!(prev, 'r' | 'R') {
is_raw_string = true;
break;
}
if !matches!(prev, 'b' | 'B' | 'f' | 'F' | 'u' | 'U') {
break;
}
}
string_quote = ch;
if i + 2 < chars.len() && chars[i + 1].1 == ch && chars[i + 2].1 == ch {
triple_quote = true;
in_string = true;
i += 3;
continue;
}
triple_quote = false;
in_string = true;
i += 1;
continue;
}
match ch {
'(' | '[' | '{' => stack.push((ch, byte_offset)),
')' | ']' | '}' => {
let expected = match ch {
')' => '(',
']' => '[',
'}' => '{',
_ => unreachable!(),
};
if stack.last().is_some_and(|&(open, _)| open == expected) {
stack.pop();
}
}
_ => {}
}
i += 1;
}
stack.last().copied()
}
/// Compile a given source code into a bytecode object.
pub fn compile(
source: &str,
+9 -19
View File
@@ -678,29 +678,19 @@ impl PyAnextAwaitable {
let awaitable = if wrapped.class().is(vm.ctx.types.coroutine_type) {
// Coroutine - get __await__ later
wrapped.clone()
} else if let Some(generator) = wrapped.downcast_ref::<PyGenerator>() {
// Generator with CO_ITERABLE_COROUTINE flag can be awaited
// (e.g., generators decorated with @types.coroutine)
if generator
.as_coro()
.frame()
.code
.flags
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
} else {
// Check for generator with CO_ITERABLE_COROUTINE flag
if let Some(generator) = wrapped.downcast_ref::<PyGenerator>()
&& generator
.as_coro()
.frame()
.code
.flags
.contains(crate::bytecode::CodeFlags::ITERABLE_COROUTINE)
{
// Return the generator itself as the iterator
return Ok(wrapped.clone());
}
// Fall through: try to get __await__ method for generator subclasses
if let Some(await_method) = vm.get_method(wrapped.clone(), identifier!(vm, __await__)) {
await_method?.call((), vm)?
} else {
return Err(vm.new_type_error(format!(
"'{}' object can't be awaited",
wrapped.class().name()
)));
}
} else {
// Try to get __await__ method
if let Some(await_method) = vm.get_method(wrapped.clone(), identifier!(vm, __await__)) {
await_method?.call((), vm)?
+40 -6
View File
@@ -6,7 +6,7 @@ use super::{PyCode, PyDictRef, PyIntRef, PyStrRef};
use crate::{
AsObject, Context, Py, PyObjectRef, PyRef, PyResult, VirtualMachine,
class::PyClassImpl,
frame::{Frame, FrameRef},
frame::{Frame, FrameOwner, FrameRef},
function::PySetterValue,
types::Representable,
};
@@ -31,11 +31,6 @@ impl Representable for Frame {
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py))]
impl Frame {
#[pymethod]
const fn clear(&self) {
// TODO
}
#[pygetset]
fn f_globals(&self) -> PyDictRef {
self.globals.clone()
@@ -151,6 +146,45 @@ impl Frame {
#[pyclass]
impl Py<Frame> {
#[pymethod]
// = frame_clear_impl
fn clear(&self, vm: &VirtualMachine) -> PyResult<()> {
let owner = FrameOwner::from_i8(self.owner.load(core::sync::atomic::Ordering::Acquire));
match owner {
FrameOwner::Generator => {
// Generator frame: check if suspended (lasti > 0 means
// FRAME_SUSPENDED). lasti == 0 means FRAME_CREATED and
// can be cleared.
if self.lasti() != 0 {
return Err(vm.new_runtime_error("cannot clear a suspended frame".to_owned()));
}
}
FrameOwner::Thread => {
// Thread-owned frame: always executing, cannot clear.
return Err(vm.new_runtime_error("cannot clear an executing frame".to_owned()));
}
FrameOwner::FrameObject => {
// Detached frame: safe to clear.
}
}
// Clear fastlocals
{
let mut fastlocals = self.fastlocals.lock();
for slot in fastlocals.iter_mut() {
*slot = None;
}
}
// Clear the evaluation stack
self.clear_value_stack();
// Clear temporary refs
self.temporary_refs.lock().clear();
Ok(())
}
#[pygetset]
fn f_generator(&self) -> Option<PyObjectRef> {
self.generator.to_owned()
+19 -3
View File
@@ -1,7 +1,7 @@
use super::PyType;
use super::{PyList, PyType};
use crate::{
AsObject, Context, Py, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl,
frame::FrameRef, types::Constructor,
frame::FrameRef, function::PySetterValue, types::Constructor,
};
use rustpython_common::lock::PyMutex;
use rustpython_compiler_core::OneIndexed;
@@ -62,12 +62,28 @@ impl PyTraceback {
self.next.lock().as_ref().cloned()
}
#[pymethod]
fn __dir__(&self, vm: &VirtualMachine) -> PyList {
PyList::from(
["tb_frame", "tb_next", "tb_lasti", "tb_lineno"]
.iter()
.map(|&s| vm.ctx.new_str(s).into())
.collect::<Vec<_>>(),
)
}
#[pygetset(setter)]
fn set_tb_next(
zelf: &Py<Self>,
value: Option<PyRef<Self>>,
value: PySetterValue<Option<PyRef<Self>>>,
vm: &VirtualMachine,
) -> PyResult<()> {
let value = match value {
PySetterValue::Assign(v) => v,
PySetterValue::Delete => {
return Err(vm.new_type_error("can't delete tb_next attribute".to_owned()));
}
};
if let Some(ref new_next) = value {
let mut cursor = new_next.clone();
loop {
+9 -2
View File
@@ -3,7 +3,7 @@ use crate::{
builtins::{PyBaseExceptionRef, PyStrRef},
common::lock::PyMutex,
exceptions::types::PyBaseException,
frame::{ExecutionResult, FrameRef},
frame::{ExecutionResult, FrameOwner, FrameRef},
function::OptionalArg,
object::{Traverse, TraverseFn},
protocol::PyIterReturn,
@@ -73,7 +73,14 @@ impl Coro {
fn maybe_close(&self, res: &PyResult<ExecutionResult>) {
match res {
Ok(ExecutionResult::Return(_)) | Err(_) => self.closed.store(true),
Ok(ExecutionResult::Return(_)) | Err(_) => {
self.closed.store(true);
// Frame is no longer suspended; allow frame.clear() to succeed.
self.frame.owner.store(
FrameOwner::FrameObject as i8,
core::sync::atomic::Ordering::Release,
);
}
Ok(ExecutionResult::Yield(_)) => {}
}
}
+117 -55
View File
@@ -58,6 +58,30 @@ struct FrameState {
lasti: u32,
}
/// Tracks who owns a frame.
// = `_PyFrameOwner`
#[repr(i8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FrameOwner {
/// Being executed by a thread (FRAME_OWNED_BY_THREAD).
Thread = 0,
/// Owned by a generator/coroutine (FRAME_OWNED_BY_GENERATOR).
Generator = 1,
/// Not executing; held only by a frame object or traceback
/// (FRAME_OWNED_BY_FRAME_OBJECT).
FrameObject = 2,
}
impl FrameOwner {
pub(crate) fn from_i8(v: i8) -> Self {
match v {
0 => Self::Thread,
1 => Self::Generator,
_ => Self::FrameObject,
}
}
}
#[cfg(feature = "threading")]
type Lasti = atomic::AtomicU32;
#[cfg(not(feature = "threading"))]
@@ -93,6 +117,10 @@ pub struct Frame {
/// Previous frame in the call chain for signal-safe traceback walking.
/// Mirrors `_PyInterpreterFrame.previous`.
pub(crate) previous: AtomicPtr<Frame>,
/// Who owns this frame. Mirrors `_PyInterpreterFrame.owner`.
/// Used by `frame.clear()` to reject clearing an executing frame,
/// even when called from a different thread.
pub(crate) owner: atomic::AtomicI8,
}
impl PyPayload for Frame {
@@ -183,18 +211,28 @@ impl Frame {
temporary_refs: PyMutex::new(vec![]),
generator: PyAtomicBorrow::new(),
previous: AtomicPtr::new(core::ptr::null_mut()),
owner: atomic::AtomicI8::new(FrameOwner::FrameObject as i8),
}
}
/// Clear the evaluation stack. Used by frame.clear() to break reference cycles.
pub(crate) fn clear_value_stack(&self) {
self.state.lock().stack.clear();
}
/// Store a borrowed back-reference to the owning generator/coroutine.
/// The caller must ensure the generator outlives the frame.
pub fn set_generator(&self, generator: &PyObject) {
self.generator.store(generator);
self.owner
.store(FrameOwner::Generator as i8, atomic::Ordering::Release);
}
/// Clear the generator back-reference. Called when the generator is finalized.
pub fn clear_generator(&self) {
self.generator.clear();
self.owner
.store(FrameOwner::FrameObject as i8, atomic::Ordering::Release);
}
pub fn current_location(&self) -> SourceLocation {
@@ -420,6 +458,7 @@ impl ExecutingFrame<'_> {
exception: PyBaseExceptionRef,
idx: usize,
is_reraise: bool,
is_new_raise: bool,
vm: &VirtualMachine,
) -> FrameResult {
// 1. Extract traceback from exception's '__traceback__' attr.
@@ -429,6 +468,11 @@ impl ExecutingFrame<'_> {
// RERAISE instructions should not add traceback entries - they're just
// re-raising an already-processed exception
if !is_reraise {
// Check if the exception already has traceback entries before
// we add ours. If it does, it was propagated from a callee
// function and we should not re-contextualize it.
let had_prior_traceback = exception.__traceback__().is_some();
// PyTraceBack_Here always adds a new entry without
// checking for duplicates. Each time an exception passes through
// a frame (e.g., in a loop with repeated raise statements),
@@ -444,13 +488,15 @@ impl ExecutingFrame<'_> {
);
vm_trace!("Adding to traceback: {:?} {:?}", new_traceback, loc.line);
exception.set_traceback_typed(Some(new_traceback.into_ref(&vm.ctx)));
}
// Only contextualize exception for new raises, not re-raises
// CPython only calls _PyErr_SetObject (which does chaining) on initial raise
// RERAISE just propagates the exception without modifying __context__
if !is_reraise {
vm.contextualize_exception(&exception);
// _PyErr_SetObject sets __context__ only when the exception
// is first raised. When an exception propagates through frames,
// __context__ must not be overwritten. We contextualize when:
// - It's an explicit raise (raise/raise from)
// - The exception had no prior traceback (originated here)
if is_new_raise || !had_prior_traceback {
vm.contextualize_exception(&exception);
}
}
// Use exception table for zero-cost exception handling
@@ -470,7 +516,18 @@ impl ExecutingFrame<'_> {
_ => false,
};
match handle_exception(self, exception, idx, is_reraise, vm) {
// Explicit raise instructions (raise/raise from) - these always
// need contextualization even if the exception has prior traceback
let is_new_raise = matches!(
op,
Instruction::RaiseVarargs { kind }
if matches!(
kind.get(arg),
bytecode::RaiseKind::Raise | bytecode::RaiseKind::RaiseCause
)
);
match handle_exception(self, exception, idx, is_reraise, is_new_raise, vm) {
Ok(None) => {}
Ok(Some(result)) => break Ok(result),
Err(exception) => {
@@ -564,6 +621,9 @@ impl ExecutingFrame<'_> {
// CLEANUP_THROW expects: [sub_iter, last_sent_val, exc]
self.push_value(vm.ctx.none());
// Set __context__ on the exception (_PyErr_ChainStackItem)
vm.contextualize_exception(&err);
// Use unwind_blocks to let exception table route to CLEANUP_THROW
match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) {
Ok(None) => self.run(vm),
@@ -2203,20 +2263,57 @@ impl ExecutingFrame<'_> {
return Ok(sub_module);
}
if is_module_initializing(module, vm) {
let module_name = module
.get_attr(identifier!(vm, __name__), vm)
.ok()
.and_then(|n| n.downcast_ref::<PyStr>().map(|s| s.as_str().to_owned()))
.unwrap_or_else(|| "<unknown>".to_owned());
// Get module name for the error message and ImportError attributes
let mod_name_obj = module.get_attr(identifier!(vm, __name__), vm).ok();
let mod_name_str = mod_name_obj
.as_ref()
.and_then(|n| n.downcast_ref::<PyStr>().map(|s| s.as_str().to_owned()));
let module_name = mod_name_str.as_deref().unwrap_or("<unknown>");
let msg = format!(
"cannot import name '{name}' from partially initialized module '{module_name}' (most likely due to a circular import)",
);
Err(vm.new_import_error(msg, name.to_owned()))
// Get module path/location for the error message
let mod_path = module
.get_attr("__spec__", vm)
.ok()
.and_then(|spec| spec.get_attr("origin", vm).ok())
.and_then(|origin| {
if vm.is_none(&origin) {
None
} else {
origin
.downcast_ref::<PyStr>()
.map(|s| s.as_str().to_owned())
}
})
.or_else(|| {
module
.get_attr(identifier!(vm, __file__), vm)
.ok()
.and_then(|f| f.downcast_ref::<PyStr>().map(|s| s.as_str().to_owned()))
});
let msg = if is_module_initializing(module, vm) {
if let Some(ref path) = mod_path {
format!(
"cannot import name '{name}' from partially initialized module \
'{module_name}' (most likely due to a circular import) ({path})",
)
} else {
format!(
"cannot import name '{name}' from partially initialized module \
'{module_name}' (most likely due to a circular import)",
)
}
} else if let Some(ref path) = mod_path {
format!("cannot import name '{name}' from '{module_name}' ({path})")
} else {
Err(vm.new_import_error(format!("cannot import name '{name}'"), name.to_owned()))
}
format!("cannot import name '{name}' from '{module_name}' (unknown location)")
};
let err = vm.new_import_error(msg, vm.ctx.new_str(module_name));
// name_from = the attribute name that failed to import (best-effort metadata)
let _ignore = err.as_object().set_attr("name_from", name.to_owned(), vm);
Err(err)
}
#[cfg_attr(feature = "flame-it", flame("Frame"))]
@@ -2294,42 +2391,7 @@ impl ExecutingFrame<'_> {
Err(exception)
}
}
UnwindReason::Returning { value } => {
// Clear tracebacks of exceptions in fastlocals to break reference cycles.
// This is needed because when returning from inside an except block,
// the exception cleanup code (e = None; del e) is skipped, leaving the
// exception with a traceback that references this frame, which references
// the exception in fastlocals, creating a cycle that can't be collected
// since RustPython doesn't have a tracing GC.
//
// We only clear tracebacks of exceptions that:
// 1. Are not the return value itself (will be needed by caller)
// 2. Are not the current active exception (still being handled)
// 3. Have a traceback whose top frame is THIS frame (we created it)
let current_exc = vm.current_exception();
let fastlocals = self.fastlocals.lock();
for obj in fastlocals.iter().flatten() {
// Skip if this object is the return value
if obj.is(&value) {
continue;
}
if let Ok(exc) = obj.clone().downcast::<PyBaseException>() {
// Skip if this is the current active exception
if current_exc.as_ref().is_some_and(|cur| exc.is(cur)) {
continue;
}
// Only clear if traceback's top frame is this frame
if exc
.__traceback__()
.is_some_and(|tb| core::ptr::eq::<Py<Frame>>(&*tb.frame, self.object))
{
exc.set_traceback_typed(None);
}
}
}
drop(fastlocals);
Ok(Some(ExecutionResult::Return(value)))
}
UnwindReason::Returning { value } => Ok(Some(ExecutionResult::Return(value))),
}
}
+4
View File
@@ -357,6 +357,7 @@ pub(crate) fn parse(
location: range.start.to_source_location(),
end_location: range.end.to_source_location(),
source_path: "<unknown>".to_string(),
is_unclosed_bracket: false,
}
})?;
@@ -368,6 +369,7 @@ pub(crate) fn parse(
location: range.start.to_source_location(),
end_location: range.end.to_source_location(),
source_path: "<unknown>".to_string(),
is_unclosed_bracket: false,
}
.into());
}
@@ -441,6 +443,7 @@ pub(crate) fn parse_func_type(
location: SourceLocation::default(),
end_location: SourceLocation::default(),
source_path: "<unknown>".to_owned(),
is_unclosed_bracket: false,
}
.into());
};
@@ -458,6 +461,7 @@ pub(crate) fn parse_func_type(
location: range.start.to_source_location(),
end_location: range.end.to_source_location(),
source_path: "<unknown>".to_string(),
is_unclosed_bracket: false,
}
})?;
Ok(*parsed.into_syntax().body)
+12 -5
View File
@@ -698,14 +698,21 @@ mod sys {
vm: &VirtualMachine,
) -> PyResult<()> {
let stderr = super::get_stderr(vm)?;
// Try to normalize the exception. If it fails, print error to stderr like CPython
match vm.normalize_exception(exc_type.clone(), exc_val.clone(), exc_tb) {
Ok(exc) => vm.write_exception(&mut crate::py_io::PyWriter(stderr, vm), &exc),
Ok(exc) => {
// Try Python traceback module first for richer output
// (enables features like keyword typo suggestions in SyntaxError)
if let Ok(tb_mod) = vm.import("traceback", 0)
&& let Ok(print_exc) = tb_mod.get_attr("print_exception", vm)
&& print_exc.call((exc.as_object().to_owned(),), vm).is_ok()
{
return Ok(());
}
// Fallback to Rust-level exception printing
vm.write_exception(&mut crate::py_io::PyWriter(stderr, vm), &exc)
}
Err(_) => {
// CPython prints error message to stderr instead of raising exception
let type_name = exc_val.class().name();
// TODO: fix error message
let msg = format!(
"TypeError: print_exception(): Exception expected for value, {type_name} found\n"
);
+27 -5
View File
@@ -48,13 +48,25 @@ pub fn calculate_suggestions<'a>(
}
pub fn offer_suggestions(exc: &Py<PyBaseException>, vm: &VirtualMachine) -> Option<PyStrRef> {
if exc.class().is(vm.ctx.exceptions.attribute_error) {
let name = exc.as_object().get_attr("name", vm).unwrap();
let obj = exc.as_object().get_attr("obj", vm).unwrap();
if exc
.class()
.fast_issubclass(vm.ctx.exceptions.attribute_error)
{
let name = exc.as_object().get_attr("name", vm).ok()?;
if vm.is_none(&name) {
return None;
}
let obj = exc.as_object().get_attr("obj", vm).ok()?;
if vm.is_none(&obj) {
return None;
}
calculate_suggestions(vm.dir(Some(obj)).ok()?.borrow_vec().iter(), &name)
} else if exc.class().is(vm.ctx.exceptions.name_error) {
let name = exc.as_object().get_attr("name", vm).unwrap();
} else if exc.class().fast_issubclass(vm.ctx.exceptions.name_error) {
let name = exc.as_object().get_attr("name", vm).ok()?;
if vm.is_none(&name) {
return None;
}
let tb = exc.__traceback__()?;
let tb = tb.iter().last().unwrap_or(tb);
@@ -70,6 +82,16 @@ pub fn offer_suggestions(exc: &Py<PyBaseException>, vm: &VirtualMachine) -> Opti
let builtins: Vec<_> = tb.frame.builtins.try_to_value(vm).ok()?;
calculate_suggestions(builtins.iter(), &name)
} else if exc.class().fast_issubclass(vm.ctx.exceptions.import_error) {
let mod_name = exc.as_object().get_attr("name", vm).ok()?;
let wrong_name = exc.as_object().get_attr("name_from", vm).ok()?;
let mod_name_str = mod_name.downcast_ref::<PyStr>()?;
// Look up the module in sys.modules
let sys_modules = vm.sys_module.get_attr("modules", vm).ok()?;
let module = sys_modules.get_item(mod_name_str.as_str(), vm).ok()?;
calculate_suggestions(vm.dir(Some(module)).ok()?.borrow_vec().iter(), &wrong_name)
} else {
None
}
+17
View File
@@ -971,7 +971,16 @@ impl VirtualMachine {
// Each frame starts with no active exception (None)
// This prevents exceptions from leaking between function calls
self.push_exception(None);
let old_owner = frame.owner.swap(
crate::frame::FrameOwner::Thread as i8,
core::sync::atomic::Ordering::AcqRel,
);
let result = f(frame);
// SAFETY: frame_ptr is valid because self.frames holds a clone
// of the frame, keeping the underlying allocation alive.
unsafe { &*frame_ptr }
.owner
.store(old_owner, core::sync::atomic::Ordering::Release);
// Pop the exception context - restores caller's exception state
self.pop_exception();
// Restore previous frame as current (unlink from chain)
@@ -1244,6 +1253,14 @@ impl VirtualMachine {
) {
if exc.class().is(self.ctx.exceptions.attribute_error) {
let exc = exc.as_object();
// Check if this exception was already augmented
let already_set = exc
.get_attr("name", self)
.ok()
.is_some_and(|v| !self.is_none(&v));
if already_set {
return;
}
exc.set_attr("name", name, self).unwrap();
exc.set_attr("obj", obj, self).unwrap();
}
+85 -22
View File
@@ -332,16 +332,54 @@ impl VirtualMachine {
source: Option<&str>,
allow_incomplete: bool,
) -> PyBaseExceptionRef {
let incomplete_or_syntax = |allow| -> &'static Py<crate::builtins::PyType> {
if allow {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
};
let syntax_error_type = match &error {
#[cfg(feature = "parser")]
// FIXME: this condition will cause TabError even when the matching actual error is IndentationError
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
ruff_python_parser::ParseErrorType::Lexical(
ruff_python_parser::LexicalErrorType::IndentationError,
),
..
}) => self.ctx.exceptions.tab_error,
}) => {
// Detect tab/space mixing to raise TabError instead of IndentationError.
// This checks both within a single line and across different lines.
let is_tab_error = source.is_some_and(|source| {
let mut has_space_indent = false;
let mut has_tab_indent = false;
for line in source.lines() {
let indent: Vec<u8> = line
.bytes()
.take_while(|&b| b == b' ' || b == b'\t')
.collect();
if indent.is_empty() {
continue;
}
if indent.contains(&b' ') && indent.contains(&b'\t') {
return true;
}
if indent.contains(&b' ') {
has_space_indent = true;
}
if indent.contains(&b'\t') {
has_tab_indent = true;
}
}
has_space_indent && has_tab_indent
});
if is_tab_error {
self.ctx.exceptions.tab_error
} else {
self.ctx.exceptions.indentation_error
}
}
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error: ruff_python_parser::ParseErrorType::UnexpectedIndentation,
@@ -354,13 +392,13 @@ impl VirtualMachine {
ruff_python_parser::LexicalErrorType::Eof,
),
..
}) => {
if allow_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
}
}) => incomplete_or_syntax(allow_incomplete),
// Unclosed bracket errors (converted from Eof by from_ruff_parse_error)
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
is_unclosed_bracket: true,
..
}) => incomplete_or_syntax(allow_incomplete),
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
@@ -370,13 +408,7 @@ impl VirtualMachine {
),
),
..
}) => {
if allow_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
}
}) => incomplete_or_syntax(allow_incomplete),
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
@@ -400,11 +432,7 @@ impl VirtualMachine {
}
}
if is_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
incomplete_or_syntax(is_incomplete)
} else {
self.ctx.exceptions.syntax_error
}
@@ -440,7 +468,7 @@ impl VirtualMachine {
if is_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.indentation_error
self.ctx.exceptions.indentation_error // not syntax_error
}
} else {
self.ctx.exceptions.indentation_error
@@ -458,6 +486,7 @@ impl VirtualMachine {
let line = source
.split('\n')
.nth(loc?.line.to_zero_indexed())?
.trim_end_matches('\r')
.to_owned();
Some(line + "\n")
}
@@ -480,8 +509,25 @@ impl VirtualMachine {
| ruff_python_parser::ParseErrorType::UnexpectedExpressionToken,
..
}) => msg.insert_str(0, "invalid syntax: "),
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
ruff_python_parser::ParseErrorType::Lexical(
ruff_python_parser::LexicalErrorType::UnrecognizedToken { .. },
)
| ruff_python_parser::ParseErrorType::SimpleStatementsOnSameLine
| ruff_python_parser::ParseErrorType::SimpleAndCompoundStatementOnSameLine
| ruff_python_parser::ParseErrorType::ExpectedToken { .. }
| ruff_python_parser::ParseErrorType::ExpectedExpression,
..
}) => {
msg = "invalid syntax".to_owned();
}
_ => {}
}
if syntax_error_type.is(self.ctx.exceptions.tab_error) {
msg = "inconsistent use of tabs and spaces in indentation".to_owned();
}
let syntax_error = self.new_exception_msg(syntax_error_type, msg);
let (lineno, offset) = error.python_location();
let lineno = self.ctx.new_int(lineno);
@@ -517,6 +563,23 @@ impl VirtualMachine {
.as_object()
.set_attr("filename", self.ctx.new_str(error.source_path()), self)
.unwrap();
// Set _metadata for keyword typo suggestions in traceback module.
// Format: (start_line, col_offset, source_code)
// start_line=0 means "include all lines from beginning" which provides
// full context needed by _find_keyword_typos to compile-check suggestions.
if let Some(source) = source {
let metadata = self.ctx.new_tuple(vec![
self.ctx.new_int(0).into(),
self.ctx.new_int(0).into(),
self.ctx.new_str(source).into(),
]);
syntax_error
.as_object()
.set_attr("_metadata", metadata, self)
.unwrap();
}
syntax_error
}