mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
234bdda40d
commit
6b870d62ad
29 files changed
+545
-245
No files matched your search
Vendored
+7
-8
@@ -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)
|
||||
Vendored
+2
@@ -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):
|
||||
|
||||
Vendored
-2
@@ -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
|
||||
|
||||
Vendored
-3
@@ -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')]
|
||||
|
||||
Vendored
+5
-11
@@ -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("""\
|
||||
|
||||
Vendored
-6
@@ -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.
|
||||
|
||||
Vendored
-2
@@ -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.
|
||||
|
||||
Vendored
-2
@@ -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
@@ -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")
|
||||
|
||||
Vendored
-1
@@ -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
@@ -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
|
||||
|
||||
Vendored
-12
@@ -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]]"
|
||||
|
||||
|
||||
Vendored
-1
@@ -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")
|
||||
|
||||
|
||||
Vendored
-1
@@ -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
|
||||
|
||||
Vendored
-27
@@ -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 = []
|
||||
|
||||
Vendored
+1
-1
@@ -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}])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user