From 1bf3c17b13d589eea6b59cfc6843f893b9df7af6 Mon Sep 17 00:00:00 2001 From: leehanjeong Date: Mon, 5 Oct 2026 16:31:32 +0900 Subject: [PATCH 1/2] Order chmod's arguments the way CPython declares them posix.chmod took dir_fd between path and mode. Since #8807 renders each argument's parameter kind, that put `/` after `*`, which inspect cannot parse, and path and mode stayed positional-only, so os.chmod(path=..., mode=...) raised TypeError. A ChmodArgs struct follows CPython's order, as nt.chmod already does. path and mode are positional-or-keyword. follow_symlinks shows CPython's text through py_default, like LinkArgs. Assisted-by: Claude Code:claude-opus-5 --- crates/vm/src/stdlib/posix.rs | 62 ++++++++++++++++------- extra_tests/snippets/builtin_signature.py | 9 ++++ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 1f123812fc2..a5fe3f4e88a 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -1186,7 +1186,7 @@ pub mod module { path: OsPath, dir_fd: DirFd<'_, 0>, mode: u32, - follow_symlinks: FollowSymlinks, + follow_symlinks: bool, vm: &VirtualMachine, ) -> PyResult<()> { let [] = dir_fd.0; @@ -1195,7 +1195,7 @@ pub mod module { not(target_os = "redox"), not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd")) ))] - if !follow_symlinks.0 { + if !follow_symlinks { let err_path = path.clone(); let c_path = path.into_cstring(vm)?; return rustpython_host_env::posix::fchmodat( @@ -1219,7 +1219,7 @@ pub mod module { let err_path = path.clone(); let body = move || { use std::os::unix::fs::PermissionsExt; - let meta = fs_metadata(&path, follow_symlinks.0)?; + let meta = fs_metadata(&path, follow_symlinks)?; let mut permissions = meta.permissions(); permissions.set_mode(mode); fs::set_permissions(&path, permissions) @@ -1233,18 +1233,32 @@ pub mod module { } #[cfg(not(target_os = "redox"))] - #[pyfunction] - fn chmod( - path: OsPathOrFd<'_>, - dir_fd: DirFd<'_, 0>, + #[derive(FromArgs)] + struct ChmodArgs<'fd> { + #[pyarg(any)] + path: OsPathOrFd<'fd>, + #[pyarg(any)] mode: u32, - follow_symlinks: FollowSymlinks, - vm: &VirtualMachine, - ) -> PyResult<()> { + #[pyarg(flatten)] + dir_fd: DirFd<'fd, 0>, + // CPython writes the platform expression; on posix it is always true. + #[pyarg(named, default = true, py_default = "(os.name != 'nt')")] + follow_symlinks: bool, + } + + #[cfg(not(target_os = "redox"))] + #[pyfunction] + fn chmod(args: ChmodArgs<'_>, vm: &VirtualMachine) -> PyResult<()> { + let ChmodArgs { + path, + mode, + dir_fd, + follow_symlinks, + } = args; match path { OsPathOrFd::Path(path) => { #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd",))] - if !follow_symlinks.0 && dir_fd == Default::default() { + if !follow_symlinks && dir_fd == Default::default() { return lchmod(LchmodArgs { path, mode }, vm); } _chmod(path, dir_fd, mode, follow_symlinks, vm) @@ -1254,14 +1268,28 @@ pub mod module { } #[cfg(target_os = "redox")] - #[pyfunction] - fn chmod( + #[derive(FromArgs)] + struct ChmodArgs { + #[pyarg(any)] path: OsPath, - dir_fd: DirFd<0>, + #[pyarg(any)] mode: u32, - follow_symlinks: FollowSymlinks, - vm: &VirtualMachine, - ) -> PyResult<()> { + #[pyarg(flatten)] + dir_fd: DirFd<'static, 0>, + // CPython writes the platform expression; on posix it is always true. + #[pyarg(named, default = true, py_default = "(os.name != 'nt')")] + follow_symlinks: bool, + } + + #[cfg(target_os = "redox")] + #[pyfunction] + fn chmod(args: ChmodArgs, vm: &VirtualMachine) -> PyResult<()> { + let ChmodArgs { + path, + mode, + dir_fd, + follow_symlinks, + } = args; _chmod(path, dir_fd, mode, follow_symlinks, vm) } diff --git a/extra_tests/snippets/builtin_signature.py b/extra_tests/snippets/builtin_signature.py index be0d53e9e3c..68998566a7e 100644 --- a/extra_tests/snippets/builtin_signature.py +++ b/extra_tests/snippets/builtin_signature.py @@ -64,3 +64,12 @@ assert sum.__text_signature__ == "($module, iterable, /, start=0)" assert str(inspect.signature(round)) == "(number, ndigits=None)" assert str(inspect.signature(sum)) == "(iterable, /, start=0)" + +# A keyword-only argument between two positional ones used to put `/` after +# `*`, which inspect cannot parse. Arguments are ordered like CPython's, so +# path and mode take keywords. Windows renders follow_symlinks differently. +if hasattr(os, "chmod"): + chmod_signature = os.chmod.__text_signature__ + assert chmod_signature.startswith("($module, /, path, mode, *, dir_fd=None"), ( + chmod_signature + ) From 465e8dc0fc2b93f01d933f8eb3dd51a83ff7b955 Mon Sep 17 00:00:00 2001 From: leehanjeong Date: Mon, 5 Oct 2026 16:31:54 +0900 Subject: [PATCH 2/2] Reject a text signature that puts `/` after `*` or `**` The writer counts positional-only parameters up front and emits `/` when they run out, which assumes they come first in the Rust argument list. Nothing enforced that, and posix.chmod violated it. The signature is composed in a const context, so the assert fails the build and names the fix. Assisted-by: Claude Code:claude-opus-5 --- crates/vm/src/function/signature.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/function/signature.rs b/crates/vm/src/function/signature.rs index 3dfae3ae9df..b43595ee358 100644 --- a/crates/vm/src/function/signature.rs +++ b/crates/vm/src/function/signature.rs @@ -242,6 +242,7 @@ struct St { emitted: bool, po_left: usize, var_pos_seen: bool, + var_kw_seen: bool, star_emitted: bool, } @@ -255,6 +256,7 @@ const fn write_signature_prefix(buf: &mut [u8], name: &str, args: &[SigArg]) -> emitted: false, po_left: count_po_args(args), var_pos_seen: false, + var_kw_seen: false, star_emitted: false, }, args, @@ -575,6 +577,12 @@ const fn write_one(buf: &mut [u8], mut st: St, param: Param, fallback: &str) -> st = emit_named(buf, st, "", name, param.default); st.po_left -= 1; if st.po_left == 0 { + // Python rejects `/` after `*` or `**`. The Rust arguments put a + // keyword or variadic one before a positional one; reorder them. + assert!( + !st.star_emitted && !st.var_pos_seen && !st.var_kw_seen, + "positional-only parameter after `*` or `**`: reorder the Rust arguments" + ); st = emit_text(buf, st, "/"); } st @@ -591,7 +599,10 @@ const fn write_one(buf: &mut [u8], mut st: St, param: Param, fallback: &str) -> st.var_pos_seen = true; emit_named(buf, st, "*", name, param.default) } - ParamKind::VarKeyword => emit_named(buf, st, "**", name, param.default), + ParamKind::VarKeyword => { + st.var_kw_seen = true; + emit_named(buf, st, "**", name, param.default) + } } } @@ -631,6 +642,7 @@ mod tests { emitted: false, po_left: 0, var_pos_seen: false, + var_kw_seen: false, star_emitted: true, }, Param {