host_env: renameat support for os.rename (#8118)

Python's `os.rename` supports renaming a path relative to a directory
descriptor - essentially, `renameat`. The syscall and its Rustix wrapper
does all of the work, so this patch mainly updates the Python signature
to forward to the implementation.

As of this patch, `os.rename` is still incorrect for Windows because it
always replaces the destination file. I added a small note for future
contributors (or myself, since I might tackle it) as a reminder of
what's broken.
This commit is contained in:
Joshua Megnauth authored and GitHub committed 2026-06-17 13:24:01 +09:00
1 parent 40197480e9
commit 697a441e70
2 files changed
+56 -8

No files matched your search

+31 -2
View File
@@ -1,7 +1,6 @@
// spell-checker:disable
// TODO: we can move more os-specific bindings/interfaces from stdlib::{os, posix, nt} to here
#[cfg(any(unix, windows, target_os = "wasi"))]
use crate::crt_fd;
#[cfg(windows)]
use crate::fs;
@@ -10,6 +9,8 @@ use core::ffi::CStr;
use core::str::Utf8Error;
#[cfg(windows)]
use core::time::Duration;
#[cfg(unix)]
use rustix::fd::AsFd;
use std::{
env,
ffi::{OsStr, OsString},
@@ -266,11 +267,39 @@ pub fn copy_file_range(
rustix::fs::copy_file_range(src, offset_src, dst, offset_dst, count)
}
#[cfg(not(unix))]
pub fn rename(
from: impl AsRef<std::path::Path>,
from_fd: Option<crt_fd::Borrowed<'_>>,
to: impl AsRef<std::path::Path>,
to_fd: Option<crt_fd::Borrowed<'_>>,
) -> io::Result<()> {
std::fs::rename(from, to)
if from_fd.is_none() && to_fd.is_none() {
// TODO: Rust's implementation always overwrites the file so ensure consistency between
// operating systems. We need to use windows-sys directly to distinguish between
// os.rename and os.replace.
std::fs::rename(from, to)
} else {
core::hint::cold_path();
Err(io::Error::new(
io::ErrorKind::Unsupported,
"renameat is not available on this platform",
))
}
}
#[cfg(unix)]
pub fn rename(
from: impl AsRef<std::path::Path>,
from_fd: Option<crt_fd::Borrowed<'_>>,
to: impl AsRef<std::path::Path>,
to_fd: Option<crt_fd::Borrowed<'_>>,
) -> io::Result<()> {
let from = from.as_ref();
let from_fd = from_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
let to = to.as_ref();
let to_fd = to_fd.as_ref().map_or(rustix::fs::CWD, AsFd::as_fd);
rustix::fs::renameat(from_fd, from, to_fd, to).map_err(Into::into)
}
#[cfg(windows)]
+25 -6
View File
@@ -189,6 +189,7 @@ pub(super) mod _os {
const UTIME_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
pub(crate) const SYMLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
pub(crate) const UNLINK_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
const RENAME_DIR_FD: bool = cfg!(unix);
const RMDIR_DIR_FD: bool = cfg!(not(any(windows, target_os = "redox")));
const SCANDIR_FD: bool = cfg!(all(unix, not(target_os = "redox")));
@@ -1377,19 +1378,37 @@ pub(super) mod _os {
FsPath::try_from_path_like(path, false, vm)
}
#[derive(FromArgs)]
struct RenameArgs<'fd> {
#[pyarg(positional)]
src: PyObjectRef,
#[pyarg(positional)]
dst: PyObjectRef,
#[pyarg(any, default)]
src_dir_fd: OptionalArg<crt_fd::Borrowed<'fd>>,
#[pyarg(any, default)]
dst_dir_fd: OptionalArg<crt_fd::Borrowed<'fd>>,
}
#[pyfunction]
#[pyfunction(name = "replace")]
fn rename(src: PyObjectRef, dst: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
fn rename(args: RenameArgs<'_>, vm: &VirtualMachine) -> PyResult<()> {
let src = PathConverter::new()
.function("rename")
.argument("src")
.try_path(src, vm)?;
.try_path(args.src, vm)?;
let dst = PathConverter::new()
.function("rename")
.argument("dst")
.try_path(dst, vm)?;
.try_path(args.dst, vm)?;
crate::host_env::os::rename(&src.path, &dst.path).map_err(|err| {
crate::host_env::os::rename(
&src,
args.src_dir_fd.into_option(),
&dst,
args.dst_dir_fd.into_option(),
)
.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));
@@ -1932,8 +1951,8 @@ pub(super) mod _os {
SupportFunc::new("readlink", Some(false), None, Some(false)),
SupportFunc::new("remove", Some(false), Some(UNLINK_DIR_FD), Some(false)),
SupportFunc::new("unlink", Some(false), Some(UNLINK_DIR_FD), Some(false)),
SupportFunc::new("rename", Some(false), None, Some(false)),
SupportFunc::new("replace", Some(false), None, Some(false)), // TODO: Fix replace
SupportFunc::new("rename", Some(false), Some(RENAME_DIR_FD), Some(false)),
SupportFunc::new("replace", Some(false), Some(RENAME_DIR_FD), Some(false)), // TODO: Fix replace
SupportFunc::new("rmdir", Some(false), Some(RMDIR_DIR_FD), Some(false)),
SupportFunc::new("scandir", Some(SCANDIR_FD), Some(false), Some(false)),
SupportFunc::new("stat", Some(true), Some(STAT_DIR_FD), Some(true)),