From 24b6f48841f9f3b41bd03936c54b5a95358e2d5e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 1 Feb 2026 19:57:41 +0900 Subject: [PATCH 1/4] Add follow_symlinks support to os.link() - Use linkat() with AT_SYMLINK_FOLLOW on Unix - Raise NotImplementedError on non-Unix when follow_symlinks=False - Register link in supports_follow_symlinks --- crates/vm/src/stdlib/os.rs | 66 ++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 691cb068605..d4a3b085bb5 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -1268,13 +1268,64 @@ pub(super) mod _os { } #[pyfunction] - fn link(src: OsPath, dst: OsPath, vm: &VirtualMachine) -> PyResult<()> { - fs::hard_link(&src.path, &dst.path).map_err(|err| { - let builder = err.to_os_error_builder(vm); - let builder = builder.filename(src.filename(vm)); - let builder = builder.filename2(dst.filename(vm)); - builder.build(vm).upcast() - }) + fn link( + src: OsPath, + dst: OsPath, + follow_symlinks: FollowSymlinks, + vm: &VirtualMachine, + ) -> PyResult<()> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let src_cstr = std::ffi::CString::new(src.path.as_os_str().as_bytes()) + .map_err(|_| vm.new_value_error("embedded null byte".to_owned()))?; + let dst_cstr = std::ffi::CString::new(dst.path.as_os_str().as_bytes()) + .map_err(|_| vm.new_value_error("embedded null byte".to_owned()))?; + + let flags = if follow_symlinks.0 { + libc::AT_SYMLINK_FOLLOW + } else { + 0 + }; + + let ret = unsafe { + libc::linkat( + libc::AT_FDCWD, + src_cstr.as_ptr(), + libc::AT_FDCWD, + dst_cstr.as_ptr(), + flags, + ) + }; + + if ret != 0 { + let err = std::io::Error::last_os_error(); + let builder = err.to_os_error_builder(vm); + let builder = builder.filename(src.filename(vm)); + let builder = builder.filename2(dst.filename(vm)); + return Err(builder.build(vm).upcast()); + } + + Ok(()) + } + + #[cfg(not(unix))] + { + // On non-Unix platforms, ignore follow_symlinks if it's the default value + // or raise NotImplementedError if explicitly set to False + if !follow_symlinks.0 { + return Err(vm.new_not_implemented_error( + "link: follow_symlinks unavailable on this platform".to_owned(), + )); + } + + fs::hard_link(&src.path, &dst.path).map_err(|err| { + let builder = err.to_os_error_builder(vm); + let builder = builder.filename(src.filename(vm)); + let builder = builder.filename2(dst.filename(vm)); + builder.build(vm).upcast() + }) + } } #[cfg(any(unix, windows))] @@ -1842,6 +1893,7 @@ pub(super) mod _os { SupportFunc::new("access", Some(false), Some(false), None), SupportFunc::new("chdir", None, Some(false), Some(false)), // chflags Some, None Some + SupportFunc::new("link", Some(false), Some(false), Some(cfg!(unix))), SupportFunc::new("listdir", Some(LISTDIR_FD), Some(false), Some(false)), SupportFunc::new("mkdir", Some(false), Some(MKDIR_DIR_FD), Some(false)), // mkfifo Some Some None From 188d438e00741dccda8484576b03041aa5ca4d61 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 1 Feb 2026 23:00:28 +0900 Subject: [PATCH 2/4] Use new_os_subtype_error for termios error construction - Replace manual exception creation with new_os_subtype_error in termios - Remove redundant .to_owned() calls in os.link() error messages --- crates/stdlib/src/termios.rs | 9 ++++----- crates/vm/src/stdlib/os.rs | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index 48dfae25a73..de402724434 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -261,13 +261,12 @@ mod termios { } fn termios_error(err: std::io::Error, vm: &VirtualMachine) -> PyBaseExceptionRef { - vm.new_exception( + vm.new_os_subtype_error( error_type(vm), - vec![ - err.posix_errno().to_pyobject(vm), - vm.ctx.new_str(err.to_string()).into(), - ], + Some(err.posix_errno()), + vm.ctx.new_str(err.to_string()), ) + .upcast() } #[pyattr(name = "error", once)] diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index d4a3b085bb5..95836e38337 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -1278,9 +1278,9 @@ pub(super) mod _os { { use std::os::unix::ffi::OsStrExt; let src_cstr = std::ffi::CString::new(src.path.as_os_str().as_bytes()) - .map_err(|_| vm.new_value_error("embedded null byte".to_owned()))?; + .map_err(|_| vm.new_value_error("embedded null byte"))?; let dst_cstr = std::ffi::CString::new(dst.path.as_os_str().as_bytes()) - .map_err(|_| vm.new_value_error("embedded null byte".to_owned()))?; + .map_err(|_| vm.new_value_error("embedded null byte"))?; let flags = if follow_symlinks.0 { libc::AT_SYMLINK_FOLLOW @@ -1315,7 +1315,7 @@ pub(super) mod _os { // or raise NotImplementedError if explicitly set to False if !follow_symlinks.0 { return Err(vm.new_not_implemented_error( - "link: follow_symlinks unavailable on this platform".to_owned(), + "link: follow_symlinks unavailable on this platform", )); } From fce2d7824fe86b736cbab5eecf1a3da53c8fbf21 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 2 Feb 2026 00:49:44 +0900 Subject: [PATCH 3/4] Propagate errors from warnings.warn() calls --- crates/vm/src/stdlib/warnings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vm/src/stdlib/warnings.rs b/crates/vm/src/stdlib/warnings.rs index ad54b806220..198df07d6c0 100644 --- a/crates/vm/src/stdlib/warnings.rs +++ b/crates/vm/src/stdlib/warnings.rs @@ -12,7 +12,7 @@ pub fn warn( if let Ok(module) = vm.import("warnings", 0) && let Ok(func) = module.get_attr("warn", vm) { - let _ = func.call((message, category.to_owned(), stack_level), vm); + func.call((message, category.to_owned(), stack_level), vm)?; } Ok(()) } From 1132f6632588581e9690ae44c877b2bc79941f3d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 1 Feb 2026 23:59:17 +0900 Subject: [PATCH 4/4] Set closefd=false for stdio file objects in VM init - Prevent closing underlying fd when stdio wrappers are dropped - Remove expectedFailure from test_fdopen in test_os.py --- Lib/test/test_os.py | 1 - crates/vm/src/vm/mod.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index f88659764bc..653a05dd011 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2350,7 +2350,6 @@ def check_bool(self, f, *args, **kwargs): with self.assertRaises(RuntimeWarning): f(fd, *args, **kwargs) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_fdopen(self): self.check(os.fdopen, encoding="utf-8") self.check_bool(os.fdopen, encoding="utf-8") diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 3a534302c31..7b5720fdd84 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -328,6 +328,7 @@ impl VirtualMachine { Some(if write { "wb" } else { "rb" }), crate::stdlib::io::OpenArgs { buffering: if unbuffered { 0 } else { -1 }, + closefd: false, ..Default::default() }, self,