From 519370bece5035ba6b54eb463fbbdd1916525c18 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sat, 10 Oct 2026 00:02:07 +0900 Subject: [PATCH] Adapt essential snippet contracts to Python 3.15 final Preserve existing coverage while following verified final behavior for script metadata, builtin signatures and loading, AST fixture construction, memoryview diagnostics, mmap search bounds, and struct format errors. The unchanged final comparator exposes the earlier expectations; focused native baseline failures establish the required implementation changes. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/stdlib/src/mmap.rs | 13 ++++-- crates/stdlib/src/pystruct.rs | 31 ++++++--------- crates/vm/src/builtins/memory.rs | 2 +- crates/vm/src/stdlib/_imp.rs | 46 +++++++++++++++++++--- crates/vm/src/stdlib/builtins.rs | 8 ++-- crates/vm/src/vm/mod.rs | 6 +-- extra_tests/snippets/builtin___main__.py | 2 +- extra_tests/snippets/builtin_memoryview.py | 2 +- extra_tests/snippets/builtin_signature.py | 4 +- extra_tests/snippets/stdlib_ast.py | 7 ++-- extra_tests/snippets/stdlib_imp.py | 3 +- extra_tests/snippets/stdlib_mmap.py | 6 +-- extra_tests/snippets/stdlib_struct.py | 18 ++++----- extra_tests/snippets/stdlib_sys.py | 2 +- 14 files changed, 91 insertions(+), 59 deletions(-) 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")