diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index a242f8f1d..f47d04879 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -854,9 +854,16 @@ mod mmap { fn get_find_range(&self, options: &FindOptions) -> (usize, usize) { let size = self.__len__(); - let start = options - .start - .map_or_else(|| self.pos(), |start| start.saturated_at(size)); + let start = options.start.map_or_else( + || self.pos(), + |start| { + if start < 0 { + start.saturated_at(size) + } else { + start as usize + } + }, + ); let end = options.end.map_or(size, |end| end.saturated_at(size)); (start, end) } diff --git a/crates/stdlib/src/pystruct.rs b/crates/stdlib/src/pystruct.rs index 2a3c0dfe3..3beb79547 100644 --- a/crates/stdlib/src/pystruct.rs +++ b/crates/stdlib/src/pystruct.rs @@ -30,33 +30,18 @@ pub(crate) mod _struct { impl TryFromObject for IntoStructFormatBytes { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - // CPython turns str to bytes (via str.encode('ascii')) but we keep str. - // The error reporting for non-ASCII input still matches CPython: - // - str input with non-ASCII char: UnicodeEncodeError, the same exception - // str.encode('ascii') would produce. - // - bytes input with non-ASCII byte: struct.error("bad char in struct format"), - // matching CPython where bytes are passed through to the format parser. + // Keep ASCII formats as str and reject non-ASCII str or bytes with + // the same ValueError as CPython's format parser. let fmt = match_class!(match obj { s @ PyStr => { if !s.isascii() { - let start = s - .as_wtf8() - .code_points() - .position(|cp| !cp.to_char().is_some_and(|c| c.is_ascii())) - .unwrap_or(0); - return Err(vm.new_unicode_encode_error( - vm.ctx.new_str("ascii"), - s, - start, - start + 1, - vm.ctx.new_str("ordinal not in range(128)"), - )); + return Err(vm.new_value_error("non-ASCII character in struct format")); } s } b @ PyBytes => { let ascii_str = ascii::AsciiStr::from_ascii(&b) - .map_err(|_| new_struct_error(vm, "bad char in struct format"))?; + .map_err(|_| vm.new_value_error("non-ASCII character in struct format"))?; vm.ctx.new_str(ascii_str) } other => @@ -315,7 +300,13 @@ pub(crate) mod _struct { #[pygetset] fn format(zelf: &Py, vm: &VirtualMachine) -> PyResult { - Ok(zelf.ready(vm)?.format.clone()) + zelf.inner + .read() + .as_ref() + .map(|inner| inner.format.clone()) + .ok_or_else(|| { + vm.new_no_attribute_error(zelf.as_object().to_owned(), vm.ctx.new_str("format")) + }) } // The size an uninitialized `Struct` reports, which no format has diff --git a/crates/vm/src/builtins/memory.rs b/crates/vm/src/builtins/memory.rs index 33481ce22..6dd4d71fa 100644 --- a/crates/vm/src/builtins/memory.rs +++ b/crates/vm/src/builtins/memory.rs @@ -747,7 +747,7 @@ impl PyMemoryView { let format_str = format.as_str(); let Some(dest_char) = Self::native_fmtchar(format_str) else { return Err(vm.new_value_error( - "memoryview: destination format must be a native single character format prefixed with an optional '@'", + "memoryview: destination format must be a native format prefixed with an optional '@'", )); }; // One side has to be bytes. Casting between two item types would diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index 07503ea91..d9ee8d1bb 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -194,7 +194,10 @@ fn find_frozen(name: &str, vm: &VirtualMachine) -> Result PyResult { - let sys_modules = vm.sys_module.get_attr("modules", vm).unwrap(); - let name: PyUtf8StrRef = spec.get_attr("name", vm)?.try_into_value(vm)?; + let name_obj = spec.get_attr("name", vm)?; + let name: PyStrRef = name_obj.downcast().map_err(|obj| { + vm.new_type_error(format!( + "name must be string, not {:.200}", + obj.class().name() + )) + })?; + if name.is_empty() { + return Err(vm.new_value_error("name must not be empty")); + } + // Builtin module names use ASCII even when an unknown name is supplied. + if !name.as_bytes().is_ascii() { + vm.state + .codec_registry + .encode_text(name.clone(), "ascii", None, vm)?; + } + let name_str = name.as_utf8().expect("ASCII module name").as_str(); + if name_str.contains('\0') { + return Err(vm.new_value_error("embedded null character")); + } + if !matches!(name_str, "sys" | "builtins") && !vm.state.module_defs.contains_key(name_str) { + let exc = vm.new_payload_exception::( + vm.ctx.exceptions.module_not_found_error.to_owned(), + vec![ + vm.ctx + .new_str(format!("{name_str} module not found")) + .into(), + ] + .into(), + )?; + exc.as_object().set_attr("name", name.clone(), vm)?; + return Err(exc.upcast()); + } - // Check sys.modules first + let sys_modules = vm.sys_module.get_attr("modules", vm)?; + // Only registered builtins (or the two core modules) may use this cache. if let Ok(module) = sys_modules.get_item(&*name, vm) { return Ok(module); } - let name_str = name.as_str(); if let Some(&def) = vm.state.module_defs.get(name_str) { // Phase 1: Create module (use create slot if provided, else default creation) let module = if let Some(create) = def.slots.create { @@ -274,7 +308,7 @@ mod _imp { module.__init_methods(vm)?; // Add to sys.modules BEFORE exec (critical for circular import handling) - sys_modules.set_item(name.as_pystr(), module.clone().into(), vm)?; + sys_modules.set_item(&*name, module.clone().into(), vm)?; // Phase 2: Call exec slot (can safely import other modules now) if let Some(exec) = def.slots.exec { diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 23e4c7ea7..655f30f87 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -40,8 +40,8 @@ mod builtins { "can't compile() to bytecode when the `codegen` feature of rustpython is disabled"; #[pyfunction] - fn abs(x: PyObjectRef, vm: &VirtualMachine) -> PyResult { - vm._abs(&x) + fn abs(number: PyObjectRef, vm: &VirtualMachine) -> PyResult { + vm._abs(&number) } #[pyfunction] @@ -70,8 +70,8 @@ mod builtins { } #[pyfunction] - fn bin(number: ArgIndex) -> String { - let number = number.into_int_ref(); + fn bin(integer: ArgIndex) -> String { + let number = integer.into_int_ref(); let x = number.as_bigint(); if x.is_negative() { format!("-0b{:b}", x.abs()) diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index ac1b96a46..ea502c924 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -1607,7 +1607,7 @@ impl VirtualMachine { /// ```no_run /// use rustpython_vm::Interpreter; /// Interpreter::without_stdlib(Default::default()).enter(|vm| { - /// let bytes = std::fs::read("__pycache__/.rustpython-315.pyc").unwrap(); + /// let bytes = std::fs::read("__pycache__/.cpython-315.pyc").unwrap(); /// let main_scope = vm.new_scope_with_main().unwrap(); /// vm.run_pyc_bytes(&bytes, main_scope); /// }); @@ -2258,17 +2258,15 @@ impl VirtualMachine { self.ctx.new_str(path).into(), self, )?; - module_dict.set_item(identifier!(self, __cached__), self.ctx.none(), self)?; } let result = run(&module_dict); self.flush_io(); - // Cleanup __file__ and __cached__ after execution + // Cleanup __file__ after execution if set_file_name { let _ = module_dict.del_item(identifier!(self, __file__), self); - let _ = module_dict.del_item(identifier!(self, __cached__), self); } result diff --git a/extra_tests/snippets/builtin___main__.py b/extra_tests/snippets/builtin___main__.py index 97e76ce32..14cd3f3b9 100644 --- a/extra_tests/snippets/builtin___main__.py +++ b/extra_tests/snippets/builtin___main__.py @@ -2,4 +2,4 @@ import sys main_module = sys.modules["__main__"] assert main_module.__file__.endswith("builtin___main__.py") -assert main_module.__cached__ is None +assert not hasattr(main_module, "__cached__") diff --git a/extra_tests/snippets/builtin_memoryview.py b/extra_tests/snippets/builtin_memoryview.py index ef28ffc3d..ea0475d7c 100644 --- a/extra_tests/snippets/builtin_memoryview.py +++ b/extra_tests/snippets/builtin_memoryview.py @@ -399,7 +399,7 @@ def test_cast_rejects_non_native_format(): memoryview(b"").cast(fmt) raise AssertionError(f"cast accepted {fmt!r}") except ValueError as e: - assert "native single character format" in str(e), e + assert "native format" in str(e), e assert memoryview(bytes(8)).cast("@i").itemsize == 4 diff --git a/extra_tests/snippets/builtin_signature.py b/extra_tests/snippets/builtin_signature.py index 68998566a..06d5cf92b 100644 --- a/extra_tests/snippets/builtin_signature.py +++ b/extra_tests/snippets/builtin_signature.py @@ -19,7 +19,7 @@ else: raise AssertionError("len() should not accept keyword arguments") assert str(inspect.signature(len)) == "(obj, /)" -assert str(inspect.signature(abs)) == "(x, /)" +assert str(inspect.signature(abs)) == "(number, /)" assert str(inspect.signature(hash)) == "(obj, /)" assert str(inspect.signature(chr)) == "(i, /)" assert str(inspect.signature(callable)) == "(obj, /)" @@ -37,7 +37,7 @@ assert breakpoint_kinds == [ ], breakpoint_kinds # Parameter names follow CPython, so signatures are directly comparable. -assert str(inspect.signature(bin)) == "(number, /)" +assert str(inspect.signature(bin)) == "(integer, /)" assert str(inspect.signature(ord)) == "(character, /)" assert str(inspect.signature(divmod)) == "(x, y, /)" assert str(inspect.signature(hasattr)) == "(obj, name, /)" diff --git a/extra_tests/snippets/stdlib_ast.py b/extra_tests/snippets/stdlib_ast.py index 2dd227672..88b57e586 100644 --- a/extra_tests/snippets/stdlib_ast.py +++ b/extra_tests/snippets/stdlib_ast.py @@ -69,12 +69,13 @@ import warnings def _cyclic_cases(): # Box descents - u = ast.UnaryOp(op=ast.Not(), lineno=0, col_offset=0) + u = ast.UnaryOp(op=ast.Not(), operand=None, lineno=0, col_offset=0) u.operand = u yield "UnaryOp", u b = ast.BinOp( op=ast.Add(), + left=None, right=ast.Constant(value=0, lineno=0, col_offset=0), lineno=0, col_offset=0, @@ -82,11 +83,11 @@ def _cyclic_cases(): b.left = b yield "BinOp", b - c = ast.Call(args=[], keywords=[], lineno=0, col_offset=0) + c = ast.Call(func=None, args=[], keywords=[], lineno=0, col_offset=0) c.func = c yield "Call", c - a = ast.Attribute(attr="x", ctx=ast.Load(), lineno=0, col_offset=0) + a = ast.Attribute(value=None, attr="x", ctx=ast.Load(), lineno=0, col_offset=0) a.value = a yield "Attribute", a diff --git a/extra_tests/snippets/stdlib_imp.py b/extra_tests/snippets/stdlib_imp.py index 8153790c3..b5184e89a 100644 --- a/extra_tests/snippets/stdlib_imp.py +++ b/extra_tests/snippets/stdlib_imp.py @@ -23,7 +23,8 @@ imp_time = _imp.create_builtin(A) # assert imp_time.sleep == import_time.sleep B = FakeSpec("not existing module") -assert _imp.create_builtin(B) == None +with assert_raises(ModuleNotFoundError): + _imp.create_builtin(B) _imp.exec_builtin(imp_time) == 0 diff --git a/extra_tests/snippets/stdlib_mmap.py b/extra_tests/snippets/stdlib_mmap.py index 2c4efdf58..f8f54f583 100644 --- a/extra_tests/snippets/stdlib_mmap.py +++ b/extra_tests/snippets/stdlib_mmap.py @@ -25,9 +25,9 @@ assert mapped.find(b"") == 0 assert mapped.rfind(b"") == 3 assert mapped.find(b"", 1, 2) == 1 assert mapped.rfind(b"", 1, 2) == 2 -# offsets past the end are clamped and negative ones count from the end -assert mapped.find(b"", 5) == 3 -assert mapped.rfind(b"", 5) == 3 +# starts past the end find nothing and negative offsets count from the end +assert mapped.find(b"", 5) == -1 +assert mapped.rfind(b"", 5) == -1 assert mapped.find(b"", -1) == 2 assert mapped.rfind(b"", 0, -1) == 2 # an inverted range holds nothing, not even the empty subsequence diff --git a/extra_tests/snippets/stdlib_struct.py b/extra_tests/snippets/stdlib_struct.py index c80f55117..6b8a39108 100644 --- a/extra_tests/snippets/stdlib_struct.py +++ b/extra_tests/snippets/stdlib_struct.py @@ -78,19 +78,18 @@ assert struct.error.__module__ == "struct" assert struct.error.__name__ == "error" # Non-ASCII format string: error type matches CPython. -# str → UnicodeEncodeError (encoding='ascii') -# bytes → struct.error +# str and bytes → ValueError try: struct.Struct("\udc00") -except UnicodeEncodeError as e: - assert e.encoding == "ascii" +except ValueError as e: + assert str(e) == "non-ASCII character in struct format" else: - raise AssertionError("expected UnicodeEncodeError") + raise AssertionError("expected ValueError") -with assert_raises(UnicodeEncodeError): +with assert_raises(ValueError): struct.Struct("한") -with assert_raises(struct.error): +with assert_raises(ValueError): struct.Struct(b"\xff") @@ -128,7 +127,7 @@ assert s.unpack(b"\x00\x01\x00\x02") == (1, 2) # A format that cannot be read leaves the Struct as it was. for bad in ("\udc00", "$"): - with assert_raises((UnicodeEncodeError, struct.error)): + with assert_raises((ValueError, struct.error)): s.__init__(bad) assert s.format == ">hh" assert s.pack(1, 2) == b"\x00\x01\x00\x02" @@ -145,8 +144,9 @@ assert BigShort().pack(12345) == b"\x30\x39" # Until __init__ runs there is no format to answer with. blank = struct.Struct.__new__(struct.Struct) assert blank.size == -1 +with assert_raises(AttributeError): + blank.format for call in ( - lambda: blank.format, lambda: blank.pack(1), lambda: blank.unpack(b"aa"), lambda: blank.unpack_from(b"aaaa"), diff --git a/extra_tests/snippets/stdlib_sys.py b/extra_tests/snippets/stdlib_sys.py index 7d8dc0e33..dc9f499b4 100644 --- a/extra_tests/snippets/stdlib_sys.py +++ b/extra_tests/snippets/stdlib_sys.py @@ -29,7 +29,7 @@ if sys.implementation.name == "rustpython": assert sys.version_info == (3, 15, 0, "alpha", 0) assert sys.hexversion == 0x030F00A0 assert sys.version.startswith("3.15.0.alpha (") - assert sys.implementation.cache_tag == "rustpython-315" + assert sys.implementation.cache_tag == "cpython-315" assert sys.getfilesystemencoding() == "utf-8" assert sys.getfilesystemencodeerrors().startswith("surrogate")