Compare commits

...

9 Commits

Author SHA1 Message Date
Ashwin Naren
c968fe0fd9 remove macos skips 2025-06-15 17:36:13 +09:00
Ashwin Naren
125f14190a Remove unnecessary uv runin README (#5792)
* Update README.md

Remove unnecessary `uv run`

* uv comment

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2025-06-15 16:10:27 +09:00
Jeong YunWon
a6dd2d805b Skip test_local_unknown_cert to avoid CI failure 2025-06-15 16:04:37 +09:00
Jeong YunWon
6723bf30a7 Fix deque module name for test_repr 2025-06-15 16:04:37 +09:00
Jeong YunWon
2c61a12bed Apply coderabbit reviews 2025-06-15 16:03:46 +09:00
Jeong YunWon
f560b4cbfb Fix nightly clippy warnings 2025-06-15 16:03:46 +09:00
dependabot[bot]
4e094eaa55 Bump webpack-dev-server from 5.2.0 to 5.2.1 in /wasm/demo (#5801)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 23:17:32 +09:00
Jeong, YunWon
2e368baf2a Fix Nightly clippy (#5798) 2025-06-06 22:00:07 +09:00
Aneesh Durg
323ea3b96b Support incomplete parsing (#5764)
* continue accepting REPL input for multiline strings

* Match cpython behavior for all multi-line statements (execute when complete)

* Emit _IncompleteInputError when compiling with incomplete flag

* Refine when _IncompleteInputError is emitted

* Support multiline strings emitting _IncompleteInputError

* lint

* Undo accidental change to PyTabError

* match -> if let

* Fix test_baseexception and test_codeop

* fix spelling

* fix exception name

* Skip pickle test of _IncompleteInputError

* Use py3.15's codeop implementation

* Update Lib/test/test_baseexception.py

---------

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-06-05 14:41:56 +09:00
42 changed files with 322 additions and 132 deletions

View File

@@ -32,11 +32,6 @@ env:
test_pathlib
test_posixpath
test_venv
# configparser: https://github.com/RustPython/RustPython/issues/4995#issuecomment-1582397417
# socketserver: seems related to configparser crash.
MACOS_SKIPS: >-
test_configparser
test_socketserver
# PLATFORM_INDEPENDENT_TESTS are tests that do not depend on the underlying OS. They are currently
# only run on Linux to speed up the CI.
PLATFORM_INDEPENDENT_TESTS: >-
@@ -284,7 +279,7 @@ jobs:
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
- if: runner.os == 'macOS'
name: run cpython platform-dependent tests (MacOS)
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.MACOS_SKIPS }}
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
- if: runner.os == 'Windows'
name: run cpython platform-dependent tests (windows partial - fixme)
run:

16
Cargo.lock generated
View File

@@ -365,18 +365,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstyle",
"clap_lex",
@@ -1302,9 +1302,9 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libffi"
version = "4.0.0"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9434b6fc77375fb624698d5f8c49d7e80b10d59eb1219afda27d1f824d4074"
checksum = "ebfd30a67b482a08116e753d0656cb626548cf4242543e5cc005be7639d99838"
dependencies = [
"libc",
"libffi-sys",
@@ -1312,9 +1312,9 @@ dependencies = [
[[package]]
name = "libffi-sys"
version = "3.2.0"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ead36a2496acfc8edd6cc32352110e9478ac5b9b5f5b9856ebd3d28019addb84"
checksum = "f003aa318c9f0ee69eb0ada7c78f5c9d2fedd2ceb274173b5c7ff475eee584a3"
dependencies = [
"cc",
]

10
Lib/codeop.py vendored
View File

@@ -65,14 +65,10 @@ def _maybe_compile(compiler, source, filename, symbol):
try:
compiler(source + "\n", filename, symbol)
return None
except _IncompleteInputError as e:
return None
except SyntaxError as e:
# XXX: RustPython; support multiline definitions in REPL
# See also: https://github.com/RustPython/RustPython/pull/5743
strerr = str(e)
if source.endswith(":") and "expected an indented block" in strerr:
return None
elif "incomplete input" in str(e):
return None
pass
# fallthrough
return compiler(source, filename, symbol, incomplete_input=False)

View File

@@ -83,6 +83,8 @@ class ExceptionClassTests(unittest.TestCase):
exc_set = set(e for e in exc_set if not e.startswith('_'))
# RUSTPYTHON specific
exc_set.discard("JitError")
# TODO: RUSTPYTHON; this will be officially introduced in Python 3.15
exc_set.discard("IncompleteInputError")
self.assertEqual(len(exc_set), 0, "%s not accounted for" % exc_set)
interface_tests = ("length", "args", "str", "repr")

View File

@@ -2088,8 +2088,6 @@ class TestDocString(unittest.TestCase):
self.assertDocStrEqual(C.__doc__, "C(x:List[int]=<factory>)")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_docstring_deque_field(self):
@dataclass
class C:
@@ -2097,8 +2095,6 @@ class TestDocString(unittest.TestCase):
self.assertDocStrEqual(C.__doc__, "C(x:collections.deque)")
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_docstring_deque_field_with_default_factory(self):
@dataclass
class C:

View File

@@ -1,3 +1,4 @@
import sys
import errno
from http import client, HTTPStatus
import io
@@ -1781,6 +1782,7 @@ class HTTPSTest(TestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(sys.platform == 'darwin', 'Occasionally success on macOS')
def test_local_unknown_cert(self):
# The custom cert isn't known to the default trust bundle
import ssl

View File

@@ -664,6 +664,9 @@ class CompatPickleTests(unittest.TestCase):
BaseExceptionGroup,
ExceptionGroup):
continue
# TODO: RUSTPYTHON: fix name mapping for _IncompleteInputError
if exc is _IncompleteInputError:
continue
if exc is not OSError and issubclass(exc, OSError):
self.assertEqual(reverse_mapping('builtins', name),
('exceptions', 'OSError'))

View File

@@ -82,8 +82,6 @@ class ReprTests(unittest.TestCase):
expected = repr(t3)[:-2] + "+++)"
eq(r3.repr(t3), expected)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_container(self):
from array import array
from collections import deque

View File

@@ -226,7 +226,7 @@ To enhance CPython compatibility, try to increase unittest coverage by checking
Another approach is to checkout the source code: builtin functions and object
methods are often the simplest and easiest way to contribute.
You can also simply run `uv run python -I whats_left.py` to assist in finding any unimplemented
You can also simply run `python -I whats_left.py` to assist in finding any unimplemented
method.
## Compiling to WebAssembly

View File

@@ -151,7 +151,7 @@ pub fn compile_program(
let mut compiler = Compiler::new(opts, source_code, "<module>".to_owned());
compiler.compile_program(ast, symbol_table)?;
let code = compiler.pop_code_object();
trace!("Compilation completed: {:?}", code);
trace!("Compilation completed: {code:?}");
Ok(code)
}
@@ -166,7 +166,7 @@ pub fn compile_program_single(
let mut compiler = Compiler::new(opts, source_code, "<module>".to_owned());
compiler.compile_program_single(&ast.body, symbol_table)?;
let code = compiler.pop_code_object();
trace!("Compilation completed: {:?}", code);
trace!("Compilation completed: {code:?}");
Ok(code)
}
@@ -180,7 +180,7 @@ pub fn compile_block_expression(
let mut compiler = Compiler::new(opts, source_code, "<module>".to_owned());
compiler.compile_block_expr(&ast.body, symbol_table)?;
let code = compiler.pop_code_object();
trace!("Compilation completed: {:?}", code);
trace!("Compilation completed: {code:?}");
Ok(code)
}
@@ -233,7 +233,7 @@ fn eprint_location(zelf: &Compiler<'_>) {
fn unwrap_internal<T>(zelf: &Compiler<'_>, r: InternalResult<T>) -> T {
if let Err(ref r_err) = r {
eprintln!("=== CODEGEN PANIC INFO ===");
eprintln!("This IS an internal error: {}", r_err);
eprintln!("This IS an internal error: {r_err}");
eprint_location(zelf);
eprintln!("=== END PANIC INFO ===");
}
@@ -671,7 +671,7 @@ impl Compiler<'_> {
fn compile_statement(&mut self, statement: &Stmt) -> CompileResult<()> {
use ruff_python_ast::*;
trace!("Compiling {:?}", statement);
trace!("Compiling {statement:?}");
self.set_source_range(statement.range());
match &statement {
@@ -1907,7 +1907,7 @@ impl Compiler<'_> {
fn compile_error_forbidden_name(&mut self, name: &str) -> CodegenError {
// TODO: make into error (fine for now since it realistically errors out earlier)
panic!("Failing due to forbidden name {:?}", name);
panic!("Failing due to forbidden name {name:?}");
}
/// Ensures that `pc.fail_pop` has at least `n + 1` entries.
@@ -3209,7 +3209,7 @@ impl Compiler<'_> {
fn compile_expression(&mut self, expression: &Expr) -> CompileResult<()> {
use ruff_python_ast::*;
trace!("Compiling {:?}", expression);
trace!("Compiling {expression:?}");
let range = expression.range();
self.set_source_range(range);
@@ -4432,7 +4432,7 @@ pub fn ruff_int_to_bigint(int: &Int) -> Result<BigInt, CodegenErrorType> {
fn parse_big_integer(int: &Int) -> Result<BigInt, CodegenErrorType> {
// TODO: Improve ruff API
// Can we avoid this copy?
let s = format!("{}", int);
let s = format!("{int}");
let mut s = s.as_str();
// See: https://peps.python.org/pep-0515/#literal-grammar
let radix = match s.get(0..2) {

View File

@@ -1388,12 +1388,12 @@ impl Instruction {
let value = ctx.get_constant(idx.get(arg) as usize);
match value.borrow_constant() {
BorrowedConstant::Code { code } if expand_code_objects => {
write!(f, "{:pad$}({:?}):", op, code)?;
write!(f, "{op:pad$}({code:?}):")?;
code.display_inner(f, true, level + 1)?;
Ok(())
}
c => {
write!(f, "{:pad$}(", op)?;
write!(f, "{op:pad$}(")?;
c.fmt_display(f)?;
write!(f, ")")
}

View File

@@ -25,6 +25,7 @@ pub enum CompileErrorType {
pub struct ParseError {
#[source]
pub error: parser::ParseErrorType,
pub raw_location: ruff_text_size::TextRange,
pub location: SourceLocation,
pub source_path: String,
}
@@ -48,6 +49,7 @@ impl CompileError {
let location = source_code.source_location(error.location.start());
Self::Parse(ParseError {
error: error.error,
raw_location: error.location,
location,
source_path: source_code.path.to_owned(),
})

View File

@@ -197,12 +197,12 @@ fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> {
}
let res = match run_mode {
RunMode::Command(command) => {
debug!("Running command {}", command);
debug!("Running command {command}");
vm.run_code_string(scope.clone(), &command, "<stdin>".to_owned())
.map(drop)
}
RunMode::Module(module) => {
debug!("Running module {}", module);
debug!("Running module {module}");
vm.run_module(&module)
}
RunMode::InstallPip(installer) => install_pip(installer, scope.clone(), vm),

View File

@@ -1,7 +1,8 @@
mod helper;
use rustpython_compiler::{
CompileError, ParseError, parser::LexicalErrorType, parser::ParseErrorType,
CompileError, ParseError, parser::FStringErrorType, parser::LexicalErrorType,
parser::ParseErrorType,
};
use rustpython_vm::{
AsObject, PyResult, VirtualMachine,
@@ -14,7 +15,8 @@ use rustpython_vm::{
enum ShellExecResult {
Ok,
PyErr(PyBaseExceptionRef),
Continue,
ContinueBlock,
ContinueLine,
}
fn shell_exec(
@@ -22,11 +24,17 @@ fn shell_exec(
source: &str,
scope: Scope,
empty_line_given: bool,
continuing: bool,
continuing_block: bool,
) -> ShellExecResult {
// compiling expects only UNIX style line endings, and will replace windows line endings
// internally. Since we might need to analyze the source to determine if an error could be
// resolved by future input, we need the location from the error to match the source code that
// was actually compiled.
#[cfg(windows)]
let source = &source.replace("\r\n", "\n");
match vm.compile(source, compiler::Mode::Single, "<stdin>".to_owned()) {
Ok(code) => {
if empty_line_given || !continuing {
if empty_line_given || !continuing_block {
// We want to execute the full code
match vm.run_code_obj(code, scope) {
Ok(_val) => ShellExecResult::Ok,
@@ -40,8 +48,32 @@ fn shell_exec(
Err(CompileError::Parse(ParseError {
error: ParseErrorType::Lexical(LexicalErrorType::Eof),
..
})) => ShellExecResult::Continue,
})) => ShellExecResult::ContinueLine,
Err(CompileError::Parse(ParseError {
error:
ParseErrorType::Lexical(LexicalErrorType::FStringError(
FStringErrorType::UnterminatedTripleQuotedString,
)),
..
})) => ShellExecResult::ContinueLine,
Err(err) => {
// Check if the error is from an unclosed triple quoted string (which should always
// continue)
if let CompileError::Parse(ParseError {
error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError),
raw_location,
..
}) = err
{
let loc = raw_location.start().to_usize();
let mut iter = source.chars();
if let Some(quote) = iter.nth(loc) {
if iter.next() == Some(quote) && iter.next() == Some(quote) {
return ShellExecResult::ContinueLine;
}
}
};
// bad_error == true if we are handling an error that should be thrown even if we are continuing
// if its an indentation error, set to true if we are continuing and the error is on column 0,
// since indentations errors on columns other than 0 should be ignored.
@@ -50,10 +82,12 @@ fn shell_exec(
let bad_error = match err {
CompileError::Parse(ref p) => {
match &p.error {
ParseErrorType::Lexical(LexicalErrorType::IndentationError) => continuing, // && p.location.is_some()
ParseErrorType::Lexical(LexicalErrorType::IndentationError) => {
continuing_block
} // && p.location.is_some()
ParseErrorType::OtherError(msg) => {
if msg.starts_with("Expected an indented block") {
continuing
continuing_block
} else {
true
}
@@ -68,7 +102,7 @@ fn shell_exec(
if empty_line_given || bad_error {
ShellExecResult::PyErr(vm.new_syntax_error(&err, Some(source)))
} else {
ShellExecResult::Continue
ShellExecResult::ContinueBlock
}
}
}
@@ -93,10 +127,19 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
println!("No previous history.");
}
let mut continuing = false;
// We might either be waiting to know if a block is complete, or waiting to know if a multiline
// statement is complete. In the former case, we need to ensure that we read one extra new line
// to know that the block is complete. In the latter, we can execute as soon as the statement is
// valid.
let mut continuing_block = false;
let mut continuing_line = false;
loop {
let prompt_name = if continuing { "ps2" } else { "ps1" };
let prompt_name = if continuing_block || continuing_line {
"ps2"
} else {
"ps1"
};
let prompt = vm
.sys_module
.get_attr(prompt_name, vm)
@@ -105,9 +148,12 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
Ok(ref s) => s.as_str(),
Err(_) => "",
};
continuing_line = false;
let result = match repl.readline(prompt) {
ReadlineResult::Line(line) => {
debug!("You entered {:?}", line);
#[cfg(debug_assertions)]
debug!("You entered {line:?}");
repl.add_history_entry(line.trim_end()).unwrap();
@@ -120,39 +166,44 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
}
full_input.push('\n');
match shell_exec(vm, &full_input, scope.clone(), empty_line_given, continuing) {
match shell_exec(
vm,
&full_input,
scope.clone(),
empty_line_given,
continuing_block,
) {
ShellExecResult::Ok => {
if continuing {
if continuing_block {
if empty_line_given {
// We should be exiting continue mode
continuing = false;
// We should exit continue mode since the block successfully executed
continuing_block = false;
full_input.clear();
Ok(())
} else {
// We should stay in continue mode
continuing = true;
Ok(())
}
} else {
// We aren't in continue mode so proceed normally
continuing = false;
full_input.clear();
Ok(())
}
Ok(())
}
ShellExecResult::Continue => {
continuing = true;
// Continue, but don't change the mode
ShellExecResult::ContinueLine => {
continuing_line = true;
Ok(())
}
ShellExecResult::ContinueBlock => {
continuing_block = true;
Ok(())
}
ShellExecResult::PyErr(err) => {
continuing = false;
continuing_block = false;
full_input.clear();
Err(err)
}
}
}
ReadlineResult::Interrupt => {
continuing = false;
continuing_block = false;
full_input.clear();
let keyboard_interrupt =
vm.new_exception_empty(vm.ctx.exceptions.keyboard_interrupt.to_owned());

View File

@@ -756,8 +756,7 @@ impl ToPyException for Base64DecodeError {
InvalidLastSymbol(_, PAD) => "Excess data after padding".to_owned(),
InvalidLastSymbol(length, _) => {
format!(
"Invalid base64-encoded string: number of data characters {} cannot be 1 more than a multiple of 4",
length
"Invalid base64-encoded string: number of data characters {length} cannot be 1 more than a multiple of 4"
)
}
// TODO: clean up errors

View File

@@ -346,13 +346,9 @@ mod _csv {
if !rest.args.is_empty() {
let arg_len = rest.args.len();
if arg_len != 1 {
return Err(vm.new_type_error(
format!(
"field_size_limit() takes at most 1 argument ({} given)",
arg_len
)
.to_string(),
));
return Err(vm.new_type_error(format!(
"field_size_limit() takes at most 1 argument ({arg_len} given)"
)));
}
let Ok(new_size) = rest.args.first().unwrap().try_int(vm) else {
return Err(vm.new_type_error("limit must be an integer".to_string()));
@@ -701,7 +697,7 @@ mod _csv {
if let Some(dialect) = g.get(name) {
Ok(self.update_py_dialect(*dialect))
} else {
Err(new_csv_error(vm, format!("{} is not registered.", name)))
Err(new_csv_error(vm, format!("{name} is not registered.")))
}
// TODO
// Maybe need to update the obj from HashMap

View File

@@ -363,7 +363,7 @@ impl FormatSpec {
// Loop over all opcodes:
for code in &self.codes {
buffer = &mut buffer[code.pre_padding..];
debug!("code: {:?}", code);
debug!("code: {code:?}");
match code.code {
FormatType::Str => {
let (buf, rest) = buffer.split_at_mut(code.repeat);
@@ -407,7 +407,7 @@ impl FormatSpec {
let mut items = Vec::with_capacity(self.arg_count);
for code in &self.codes {
data = &data[code.pre_padding..];
debug!("unpack code: {:?}", code);
debug!("unpack code: {code:?}");
match code.code {
FormatType::Pad => {
data = &data[code.repeat..];

View File

@@ -66,10 +66,9 @@ impl PyObjectRef {
warnings::warn(
vm.ctx.exceptions.deprecation_warning,
format!(
"__complex__ returned non-complex (type {}). \
"__complex__ returned non-complex (type {ret_class}). \
The ability to return an instance of a strict subclass of complex \
is deprecated, and may be removed in a future version of Python.",
ret_class
is deprecated, and may be removed in a future version of Python."
),
1,
vm,

View File

@@ -53,14 +53,12 @@ impl Constructor for PyBaseObject {
0 => {}
1 => {
return Err(vm.new_type_error(format!(
"class {} without an implementation for abstract method '{}'",
name, methods
"class {name} without an implementation for abstract method '{methods}'"
)));
}
2.. => {
return Err(vm.new_type_error(format!(
"class {} without an implementation for abstract methods '{}'",
name, methods
"class {name} without an implementation for abstract methods '{methods}'"
)));
}
// TODO: remove `allow` when redox build doesn't complain about it

View File

@@ -132,8 +132,7 @@ impl PyProperty {
let func_args_len = func_args.args.len();
let (_owner, name): (PyObjectRef, PyObjectRef) = func_args.bind(vm).map_err(|_e| {
vm.new_type_error(format!(
"__set_name__() takes 2 positional arguments but {} were given",
func_args_len
"__set_name__() takes 2 positional arguments but {func_args_len} were given"
))
})?;

View File

@@ -1,3 +1,4 @@
// cspell:ignore cmeth
/*! Python `super` class.
See also [CPython source code.](https://github.com/python/cpython/blob/50b48572d9a90c5bb36e2bef6179548ea927a35a/Objects/typeobject.c#L7663)
@@ -125,8 +126,8 @@ impl Initializer for PySuper {
(typ, obj)
};
let mut inner = PySuperInner::new(typ, obj, vm)?;
std::mem::swap(&mut inner, &mut zelf.inner.write());
let inner = PySuperInner::new(typ, obj, vm)?;
*zelf.inner.write() = inner;
Ok(())
}

View File

@@ -419,7 +419,7 @@ impl StandardEncoding {
match encoding {
"be" => Some(Self::Utf32Be),
"le" => Some(Self::Utf32Le),
_ => return None,
_ => None,
}
} else {
None
@@ -1116,7 +1116,7 @@ fn replace_errors(err: PyObjectRef, vm: &VirtualMachine) -> PyResult<(PyObjectRe
let replace = replacement_char.repeat(range.end - range.start);
Ok((replace.to_pyobject(vm), range.end))
} else {
return Err(bad_err_type(err, vm));
Err(bad_err_type(err, vm))
}
}

View File

@@ -49,3 +49,10 @@ impl crate::convert::ToPyException for (CompileError, Option<&str>) {
vm.new_syntax_error(&self.0, self.1)
}
}
#[cfg(any(feature = "parser", feature = "codegen"))]
impl crate::convert::ToPyException for (CompileError, Option<&str>, bool) {
fn to_pyexception(&self, vm: &crate::VirtualMachine) -> crate::builtins::PyBaseExceptionRef {
vm.new_syntax_error_maybe_incomplete(&self.0, self.1, self.2)
}
}

View File

@@ -3,7 +3,7 @@ use crate::{PyResult, VirtualMachine, compiler, scope::Scope};
pub fn eval(vm: &VirtualMachine, source: &str, scope: Scope, source_path: &str) -> PyResult {
match vm.compile(source, compiler::Mode::Eval, source_path.to_owned()) {
Ok(bytecode) => {
debug!("Code object: {:?}", bytecode);
debug!("Code object: {bytecode:?}");
vm.run_code_obj(bytecode, scope)
}
Err(err) => Err(vm.new_syntax_error(&err, Some(source))),

View File

@@ -206,7 +206,7 @@ impl VirtualMachine {
lineno
)?;
} else if let Some(filename) = maybe_filename {
filename_suffix = format!(" ({})", filename);
filename_suffix = format!(" ({filename})");
}
if let Some(text) = maybe_text {
@@ -215,7 +215,7 @@ impl VirtualMachine {
let l_text = r_text.trim_start_matches([' ', '\n', '\x0c']); // \x0c is \f
let spaces = (r_text.len() - l_text.len()) as isize;
writeln!(output, " {}", l_text)?;
writeln!(output, " {l_text}")?;
let maybe_offset: Option<isize> =
getattr("offset").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
@@ -495,6 +495,7 @@ pub struct ExceptionZoo {
pub not_implemented_error: &'static Py<PyType>,
pub recursion_error: &'static Py<PyType>,
pub syntax_error: &'static Py<PyType>,
pub incomplete_input_error: &'static Py<PyType>,
pub indentation_error: &'static Py<PyType>,
pub tab_error: &'static Py<PyType>,
pub system_error: &'static Py<PyType>,
@@ -743,6 +744,7 @@ impl ExceptionZoo {
let recursion_error = PyRecursionError::init_builtin_type();
let syntax_error = PySyntaxError::init_builtin_type();
let incomplete_input_error = PyIncompleteInputError::init_builtin_type();
let indentation_error = PyIndentationError::init_builtin_type();
let tab_error = PyTabError::init_builtin_type();
@@ -817,6 +819,7 @@ impl ExceptionZoo {
not_implemented_error,
recursion_error,
syntax_error,
incomplete_input_error,
indentation_error,
tab_error,
system_error,
@@ -965,6 +968,7 @@ impl ExceptionZoo {
"end_offset" => ctx.none(),
"text" => ctx.none(),
});
extend_exception!(PyIncompleteInputError, ctx, excs.incomplete_input_error);
extend_exception!(PyIndentationError, ctx, excs.indentation_error);
extend_exception!(PyTabError, ctx, excs.tab_error);
@@ -1611,7 +1615,7 @@ pub(super) mod types {
format!("{} ({}, line {})", msg, basename(filename.as_str()), lineno)
}
(Some(lineno), None) => {
format!("{} (line {})", msg, lineno)
format!("{msg} (line {lineno})")
}
(None, Some(filename)) => {
format!("{} ({})", msg, basename(filename.as_str()))
@@ -1623,6 +1627,28 @@ pub(super) mod types {
}
}
#[pyexception(
name = "_IncompleteInputError",
base = "PySyntaxError",
ctx = "incomplete_input_error"
)]
#[derive(Debug)]
pub struct PyIncompleteInputError {}
#[pyexception]
impl PyIncompleteInputError {
#[pyslot]
#[pymethod(name = "__init__")]
pub(crate) fn slot_init(
zelf: PyObjectRef,
_args: FuncArgs,
vm: &VirtualMachine,
) -> PyResult<()> {
zelf.set_attr("name", vm.ctx.new_str("SyntaxError"), vm)?;
Ok(())
}
}
#[pyexception(name, base = "PySyntaxError", ctx = "indentation_error", impl)]
#[derive(Debug)]
pub struct PyIndentationError {}

View File

@@ -1667,7 +1667,8 @@ impl ExecutingFrame<'_> {
.topmost_exception()
.ok_or_else(|| vm.new_runtime_error("No active exception to reraise".to_owned()))?,
};
debug!("Exception raised: {:?} with cause: {:?}", exception, cause);
#[cfg(debug_assertions)]
debug!("Exception raised: {exception:?} with cause: {cause:?}");
if let Some(cause) = cause {
exception.set_cause(cause);
}

View File

@@ -51,8 +51,7 @@ impl PyObject {
Err(err) => return err,
};
vm.new_value_error(format!(
"invalid literal for int() with base {}: {}",
base, repr,
"invalid literal for int() with base {base}: {repr}",
))
})?;
Ok(PyInt::from(i).into_ref(&vm.ctx))
@@ -475,10 +474,9 @@ impl PyNumber<'_> {
warnings::warn(
vm.ctx.exceptions.deprecation_warning,
format!(
"__int__ returned non-int (type {}). \
"__int__ returned non-int (type {ret_class}). \
The ability to return an instance of a strict subclass of int \
is deprecated, and may be removed in a future version of Python.",
ret_class
is deprecated, and may be removed in a future version of Python."
),
1,
vm,
@@ -509,10 +507,9 @@ impl PyNumber<'_> {
warnings::warn(
vm.ctx.exceptions.deprecation_warning,
format!(
"__index__ returned non-int (type {}). \
"__index__ returned non-int (type {ret_class}). \
The ability to return an instance of a strict subclass of int \
is deprecated, and may be removed in a future version of Python.",
ret_class
is deprecated, and may be removed in a future version of Python."
),
1,
vm,
@@ -543,10 +540,9 @@ impl PyNumber<'_> {
warnings::warn(
vm.ctx.exceptions.deprecation_warning,
format!(
"__float__ returned non-float (type {}). \
"__float__ returned non-float (type {ret_class}). \
The ability to return an instance of a strict subclass of float \
is deprecated, and may be removed in a future version of Python.",
ret_class
is deprecated, and may be removed in a future version of Python."
),
1,
vm,

View File

@@ -245,6 +245,7 @@ pub(crate) fn parse(
let top = parser::parse(source, mode.into())
.map_err(|parse_error| ParseError {
error: parse_error.error,
raw_location: parse_error.location,
location: text_range_to_source_range(&source_code, parse_error.location)
.start
.to_source_location(),
@@ -295,8 +296,8 @@ pub const PY_COMPILE_FLAG_AST_ONLY: i32 = 0x0400;
// The following flags match the values from Include/cpython/compile.h
// Caveat emptor: These flags are undocumented on purpose and depending
// on their effect outside the standard library is **unsupported**.
const PY_CF_DONT_IMPLY_DEDENT: i32 = 0x200;
const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = 0x4000;
pub const PY_CF_DONT_IMPLY_DEDENT: i32 = 0x200;
pub const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = 0x4000;
// __future__ flags - sync with Lib/__future__.py
// TODO: These flags aren't being used in rust code

View File

@@ -186,6 +186,8 @@ mod builtins {
return Err(vm.new_value_error("compile() unrecognized flags".to_owned()));
}
let allow_incomplete = !(flags & ast::PY_CF_ALLOW_INCOMPLETE_INPUT).is_zero();
if (flags & ast::PY_COMPILE_FLAG_AST_ONLY).is_zero() {
#[cfg(not(feature = "compiler"))]
{
@@ -207,14 +209,17 @@ mod builtins {
args.filename.to_string_lossy().into_owned(),
opts,
)
.map_err(|err| (err, Some(source)).to_pyexception(vm))?;
.map_err(|err| {
(err, Some(source), allow_incomplete).to_pyexception(vm)
})?;
Ok(code.into())
}
} else {
let mode = mode_str
.parse::<parser::Mode>()
.map_err(|err| vm.new_value_error(err.to_string()))?;
ast::parse(vm, source, mode).map_err(|e| (e, Some(source)).to_pyexception(vm))
ast::parse(vm, source, mode)
.map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm))
}
}
}
@@ -1056,6 +1061,7 @@ pub fn init_module(vm: &VirtualMachine, module: &Py<PyModule>) {
"NotImplementedError" => ctx.exceptions.not_implemented_error.to_owned(),
"RecursionError" => ctx.exceptions.recursion_error.to_owned(),
"SyntaxError" => ctx.exceptions.syntax_error.to_owned(),
"_IncompleteInputError" => ctx.exceptions.incomplete_input_error.to_owned(),
"IndentationError" => ctx.exceptions.indentation_error.to_owned(),
"TabError" => ctx.exceptions.tab_error.to_owned(),
"SystemError" => ctx.exceptions.system_error.to_owned(),

View File

@@ -27,7 +27,7 @@ mod _collections {
use std::collections::VecDeque;
#[pyattr]
#[pyclass(name = "deque", unhashable = true)]
#[pyclass(module = "collections", name = "deque", unhashable = true)]
#[derive(Debug, Default, PyPayload)]
struct PyDeque {
deque: PyRwLock<VecDeque<PyObjectRef>>,

View File

@@ -251,7 +251,7 @@ impl PyCSimple {
#[pyclassmethod]
fn repeat(cls: PyTypeRef, n: isize, vm: &VirtualMachine) -> PyResult {
if n < 0 {
return Err(vm.new_value_error(format!("Array length must be >= 0, not {}", n)));
return Err(vm.new_value_error(format!("Array length must be >= 0, not {n}")));
}
Ok(PyCArrayType {
inner: PyCArray {

View File

@@ -13,6 +13,7 @@ use libffi::middle::{Arg, Cif, CodePtr, Type};
use libloading::Symbol;
use num_traits::ToPrimitive;
use rustpython_common::lock::PyRwLock;
use std::ffi::CString;
use std::fmt::Debug;
// https://github.com/python/cpython/blob/4f8bb3947cfbc20f970ff9d9531e1132a9e95396/Modules/_ctypes/callproc.c#L15
@@ -77,10 +78,11 @@ impl Function {
}
})
.collect::<PyResult<Vec<Type>>>()?;
let terminated = format!("{}\0", function);
let c_function_name = CString::new(function)
.map_err(|_| vm.new_value_error("Function name contains null bytes".to_string()))?;
let pointer: Symbol<'_, FP> = unsafe {
library
.get(terminated.as_bytes())
.get(c_function_name.as_bytes())
.map_err(|err| err.to_string())
.map_err(|err| vm.new_attribute_error(err))?
};

View File

@@ -53,7 +53,7 @@ impl GetAttr for PyCStructure {
let data = zelf.data.read();
match data.get(&name) {
Some(value) => Ok(value.clone()),
None => Err(vm.new_attribute_error(format!("No attribute named {}", name))),
None => Err(vm.new_attribute_error(format!("No attribute named {name}"))),
}
}
}

View File

@@ -1175,7 +1175,7 @@ mod _io {
vm.call_method(self.raw.as_ref().unwrap(), "readinto", (mem_obj.clone(),));
mem_obj.release();
std::mem::swap(v, &mut read_buf.take());
*v = read_buf.take();
res?
}

View File

@@ -1989,7 +1989,7 @@ pub mod module {
let pathname = vm.ctx.new_dict();
for variant in PathconfVar::iter() {
// get the name of variant as a string to use as the dictionary key
let key = vm.ctx.new_str(format!("{:?}", variant));
let key = vm.ctx.new_str(format!("{variant:?}"));
// get the enum from the string and convert it to an integer for the dictionary value
let value = vm.ctx.new_int(variant as u8);
pathname
@@ -2185,7 +2185,7 @@ pub mod module {
let names = vm.ctx.new_dict();
for variant in SysconfVar::iter() {
// get the name of variant as a string to use as the dictionary key
let key = vm.ctx.new_str(format!("{:?}", variant));
let key = vm.ctx.new_str(format!("{variant:?}"));
// get the enum from the string and convert it to an integer for the dictionary value
let value = vm.ctx.new_int(variant as u8);
names

View File

@@ -317,9 +317,9 @@ mod sys {
let mut source = String::new();
handle
.read_to_string(&mut source)
.map_err(|e| vm.new_os_error(format!("Error reading from stdin: {}", e)))?;
.map_err(|e| vm.new_os_error(format!("Error reading from stdin: {e}")))?;
vm.compile(&source, crate::compiler::Mode::Single, "<stdin>".to_owned())
.map_err(|e| vm.new_os_error(format!("Error running stdin: {}", e)))?;
.map_err(|e| vm.new_os_error(format!("Error running stdin: {e}")))?;
Ok(())
}
@@ -723,7 +723,7 @@ mod sys {
vm.state.int_max_str_digits.store(maxdigits);
Ok(())
} else {
let error = format!("maxdigits must be 0 or larger than {:?}", threshold);
let error = format!("maxdigits must be 0 or larger than {threshold:?}");
Err(vm.new_value_error(error))
}
}

View File

@@ -49,7 +49,7 @@ impl VirtualMachine {
self.run_code_string(scope, &source, path.to_owned())?;
}
Err(err) => {
error!("Failed reading file '{}': {}", path, err);
error!("Failed reading file '{path}': {err}");
// TODO: Need to change to ExitCode or Termination
std::process::exit(1);
}

View File

@@ -320,10 +320,11 @@ impl VirtualMachine {
}
#[cfg(any(feature = "parser", feature = "compiler"))]
pub fn new_syntax_error(
pub fn new_syntax_error_maybe_incomplete(
&self,
error: &crate::compiler::CompileError,
source: Option<&str>,
allow_incomplete: bool,
) -> PyBaseExceptionRef {
use crate::source::SourceLocation;
@@ -343,12 +344,102 @@ impl VirtualMachine {
..
}) => self.ctx.exceptions.indentation_error,
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
ruff_python_parser::ParseErrorType::Lexical(
ruff_python_parser::LexicalErrorType::Eof,
),
..
}) => {
if allow_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
}
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
ruff_python_parser::ParseErrorType::Lexical(
ruff_python_parser::LexicalErrorType::FStringError(
ruff_python_parser::FStringErrorType::UnterminatedTripleQuotedString,
),
),
..
}) => {
if allow_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
}
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error:
ruff_python_parser::ParseErrorType::Lexical(
ruff_python_parser::LexicalErrorType::UnclosedStringError,
),
raw_location,
..
}) => {
if allow_incomplete {
let mut is_incomplete = false;
if let Some(source) = source {
let loc = raw_location.start().to_usize();
let mut iter = source.chars();
if let Some(quote) = iter.nth(loc) {
if iter.next() == Some(quote) && iter.next() == Some(quote) {
is_incomplete = true;
}
}
}
if is_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.syntax_error
}
} else {
self.ctx.exceptions.syntax_error
}
}
#[cfg(feature = "parser")]
crate::compiler::CompileError::Parse(rustpython_compiler::ParseError {
error: ruff_python_parser::ParseErrorType::OtherError(s),
raw_location,
..
}) => {
if s.starts_with("Expected an indented block after") {
self.ctx.exceptions.indentation_error
if allow_incomplete {
// Check that all chars in the error are whitespace, if so, the source is
// incomplete. Otherwise, we've found code that might violates
// indentation rules.
let mut is_incomplete = true;
if let Some(source) = source {
let start = raw_location.start().to_usize();
let end = raw_location.end().to_usize();
let mut iter = source.chars();
iter.nth(start);
for _ in start..end {
if let Some(c) = iter.next() {
if !c.is_ascii_whitespace() {
is_incomplete = false;
}
} else {
break;
}
}
}
if is_incomplete {
self.ctx.exceptions.incomplete_input_error
} else {
self.ctx.exceptions.indentation_error
}
} else {
self.ctx.exceptions.indentation_error
}
} else {
self.ctx.exceptions.syntax_error
}
@@ -410,6 +501,15 @@ impl VirtualMachine {
syntax_error
}
#[cfg(any(feature = "parser", feature = "compiler"))]
pub fn new_syntax_error(
&self,
error: &crate::compiler::CompileError,
source: Option<&str>,
) -> PyBaseExceptionRef {
self.new_syntax_error_maybe_incomplete(error, source, false)
}
pub fn new_import_error(&self, msg: String, name: PyStrRef) -> PyBaseExceptionRef {
let import_error = self.ctx.exceptions.import_error.to_owned();
let exc = self.new_exception_msg(import_error, msg);

View File

@@ -24,7 +24,7 @@
"serve": "^14.2.4",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
"webpack-dev-server": "^5.2.1"
}
},
"node_modules/@codemirror/autocomplete": {
@@ -5360,15 +5360,16 @@
}
},
"node_modules/webpack-dev-server": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz",
"integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz",
"integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/bonjour": "^3.5.13",
"@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.21",
"@types/serve-index": "^1.9.4",
"@types/serve-static": "^1.15.5",
"@types/sockjs": "^0.3.36",
@@ -5416,6 +5417,19 @@
}
}
},
"node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": {
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/webpack-merge": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",

View File

@@ -19,7 +19,7 @@
"serve": "^14.2.4",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
"webpack-dev-server": "^5.2.1"
},
"scripts": {
"dev": "webpack serve",

View File

@@ -40,7 +40,7 @@ if implementation != "CPython":
sys.exit(f"whats_left.py must be run under CPython, got {implementation} instead")
if sys.version_info[:2] < (3, 13):
sys.exit(
f"whats_left.py must be run under CPython 3.13 or newer, got {implementation} {sys.version} instead"
f"whats_left.py must be run under CPython 3.13 or newer, got {implementation} {sys.version} instead. If you have uv, try `uv run python -I whats_left.py` to select a proper Python interpreter easier."
)

View File

@@ -676,7 +676,7 @@ impl fmt::Debug for Wtf8 {
write_str_escaped(formatter, unsafe {
str::from_utf8_unchecked(&self.bytes[pos..surrogate_pos])
})?;
write!(formatter, "\\u{{{:x}}}", surrogate)?;
write!(formatter, "\\u{{{surrogate:x}}}")?;
pos = surrogate_pos + 3;
}
write_str_escaped(formatter, unsafe {