8000 impl os functions by youknowone · Pull Request #6484 · RustPython/RustPython · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,15 +1725,15 @@ def walk(self, top, **kwargs):
bdirs[:] = list(map(os.fsencode, dirs))
bfiles[:] = list(map(os.fsencode, files))

@unittest.expectedFailure # TODO: RUSTPYTHON; (TypeError: Can't mix strings and bytes in path components)
@unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods
def test_compare_to_walk(self):
return super().test_compare_to_walk()

@unittest.expectedFailure # TODO: RUSTPYTHON; (TypeError: Can't mix strings and bytes in path components)
@unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods
def test_dir_fd(self):
return super().test_dir_fd()

@unittest.expectedFailure # TODO: RUSTPYTHON; (TypeError: Can't mix strings and bytes in path components)
@unittest.expectedFailure # TODO: RUSTPYTHON; WalkTests doesn't have these methods
def test_yields_correct_dir_fd(self):
return super().test_yields_correct_dir_fd()

Expand Down Expand Up @@ -4502,7 +4502,6 @@ class Str(str):

self.filenames = self.bytes_filenames + self.unicode_filenames

@unittest.expectedFailure # TODO: RUSTPYTHON; (AssertionError: b'@test_22106_tmp\xe7w\xf0' is not b'@test_22106_tmp\xe7w\xf0' : <built-in function chdir>)
def test_oserror_filename(self):
funcs = [
(self.filenames, os.chdir,),
Expand Down Expand Up @@ -4906,7 +4905,6 @@ def setUp(self):
def test_uninstantiable(self):
self.assertRaises(TypeError, os.DirEntry)

@unittest.expectedFailure # TODO: RUSTPYTHON; (pickle.PicklingError: Can't pickle <class '_os.DirEntry'>: it's not found as _os.DirEntry)
def test_unpickable(self):
filename = create_file(os.path.join(self.path, "file.txt"), b'python')
entry = [entry for entry in os.scandir(self.path)].pop()
Expand Down Expand Up @@ -5337,7 +5335,6 @@ def __fspath__(self):
return ''
self.assertFalse(hasattr(A(), '__dict__'))

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_fspath_set_to_None(self):
class Foo:
__fspath__ = None
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/function/fspath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
};
use std::{borrow::Cow, ffi::OsStr, path::PathBuf};

/// Helper to implement os.fspath()
#[derive(Clone)]
pub enum FsPath {
Str(PyStrRef),
Expand All @@ -27,7 +28,7 @@ impl FsPath {
)
}

// PyOS_FSPath in CPython
// PyOS_FSPath
pub fn try_from(
obj: PyObjectRef,
check_for_nul: bool,
Expand Down
217 changes: 188 additions & 29 deletions crates/vm/src/ospath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,181 @@ use rustpython_common::crt_fd;

use crate::{
PyObjectRef, PyResult, VirtualMachine,
builtins::{PyBytes, PyStr},
convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject},
function::FsPath,
};
use std::path::{Path, PathBuf};

// path_ without allow_fd in CPython
/// path_converter
#[derive(Clone, Copy, Default)]
pub struct PathConverter {
/// Function name for erro 8000 r messages (e.g., "rename")
pub function_name: Option<&'static str>,
/// Argument name for error messages (e.g., "src", "dst")
pub argument_name: Option<&'static str>,
/// If true, embedded null characters are allowed
pub non_strict: bool,
}

impl PathConverter {
pub const fn new() -> Self {
Self {
function_name: None,
argument_name: None,
non_strict: false,
}
}

pub const fn function(mut self, name: &'static str) -> Self {
self.function_name = Some(name);
self
}

pub const fn argument(mut self, name: &'static str) -> Self {
self.argument_name = Some(name);
self
}

pub const fn non_strict(mut self) -> Self {
self.non_strict = true;
self
}

/// Generate error message prefix like "rename: "
fn error_prefix(&self) -> String {
match self.function_name {
Some(func) => format!("{}: ", func),
None => String::new(),
}
}

/// Get argument name for error messages, defaults to "path"
fn arg_name(&self) -> &'static str {
self.argument_name.unwrap_or("path")
}

/// Format a type error message
fn type_error_msg(&self, type_name: &str, allow_fd: bool) -> String {
let expected = if allow_fd {
"string, bytes, os.PathLike or integer"
} else {
"string, bytes or os.PathLike"
};
format!(
"{}{} should be {}, not {}",
self.error_prefix(),
self.arg_name(),
expected,
type_name
)
}

/// Convert to OsPathOrFd (path or file descriptor)
pub(crate) fn try_path_or_fd<'fd>(
&self,
obj: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<OsPathOrFd<'fd>> {
// Handle fd (before __fspath__ check, like CPython)
if let Some(int) = obj.try_index_opt(vm) {
let fd = int?.try_to_primitive(vm)?;
return unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }
.map(OsPathOrFd::Fd)
.map_err(|e| e.into_pyexception(vm));
}

self.try_path_inner(obj, true, vm).map(OsPathOrFd::Path)
}

/// Convert to OsPath only (no fd support)
fn try_path_inner(
&self,
obj: PyObjectRef,
allow_fd: bool,
vm: &VirtualMachine,
) -> PyResult<OsPath> {
// Try direct str/bytes match
let obj = match self.try_match_str_bytes(obj.clone(), vm)? {
Ok(path) => return Ok(path),
Err(obj) => obj,
};

// Call __fspath__
let type_error_msg = || self.type_error_msg(&obj.class().name(), allow_fd);
let method =
vm.get_method_or_type_error(obj.clone(), identifier!(vm, __fspath__), type_error_msg)?;
if vm.is_none(&method) {
return Err(vm.new_type_error(type_error_msg()));
}
let result = method.call((), vm)?;

// Match __fspath__ result
self.try_match_str_bytes(result.clone(), vm)?.map_err(|_| {
vm.new_type_error(format!(
"{}expected {}.__fspath__() to return str or bytes, not {}",
self.error_prefix(),
obj.class().name(),
result.class().name(),
))
})
}

/// Try to match str or bytes, returns Err(obj) if neither
fn try_match_str_bytes(
&self,
obj: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<Result<OsPath, PyObjectRef>> {
let check_nul = |b: &[u8]| {
if self.non_strict || memchr::memchr(b'\0', b).is_none() {
Ok(())
} else {
Err(vm.new_value_error(format!(
"{}embedded null character in {}",
self.error_prefix(),
self.arg_name()
)))
}
};

match_class!(match obj {
s @ PyStr => {
check_nul(s.as_bytes())?;
let path = vm.fsencode(&s)?.into_owned();
Ok(Ok(OsPath {
path,
origin: Some(s.into()),
}))
}
b @ PyBytes => {
check_nul(&b)?;
let path = FsPath::bytes_as_os_str(&b, vm)?.to_owned();
Ok(Ok(OsPath {
path,
origin: Some(b.into()),
}))
}
obj => Ok(Err(obj)),
})
}

/// Convert to OsPath directly
pub fn try_path(&self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<OsPath> {
self.try_path_inner(obj, false, vm)
}
}

/// path_t output - the converted path
#[derive(Clone)]
pub struct OsPath {
pub path: std::ffi::OsString,
pub(super) mode: OutputMode,
/// Original Python object for identity preservation in OSError
pub(super) origin: Option<PyObjectRef>,
}

#[derive(Debug, Copy, Clone)]
pub(super) enum OutputMode {
pub enum OutputMode {
String,
Bytes,
}
Expand All @@ -38,19 +199,19 @@ impl OutputMode {
impl OsPath {
pub fn new_str(path: impl Into<std::ffi::OsString>) -> Self {
let path = path.into();
Self {
path,
mode: OutputMode::String,
}
Self { path, origin: None }
}

pub(crate) fn from_fspath(fspath: FsPath, vm: &VirtualMachine) -> PyResult<Self> {
let path = fspath.as_os_str(vm)?.into_owned();
let mode = match fspath {
FsPath::Str(_) => OutputMode::String,
FsPath::Bytes(_) => OutputMode::Bytes,
let origin = match fspath {
FsPath::Str(s) => s.into(),
FsPath::Bytes(b) => b.into(),
};
Ok(Self { path, mode })
Ok(Self {
path,
origin: Some(origin),
})
}

/// Convert an object to OsPath using the os.fspath-style error message.
Expand Down Expand Up @@ -83,7 +244,20 @@ impl OsPath {
}

pub fn filename(&self, vm: &VirtualMachine) -> PyObjectRef {
self.mode.process_path(self.path.clone(), vm)
if let Some(ref origin) = self.origin {
origin.clone()
} else {
// Default to string when no origin (e.g., from new_str)
OutputMode::String.process_path(self.path.clone(), vm)
}
}

/// Get the output mode based on origin type (bytes -> Bytes, otherwise -> String)
pub fn mode(&self) -> OutputMode {
match &self.origin {
Some(obj) if obj.downcast_ref::<PyBytes>().is_some() => OutputMode::Bytes,
_ => OutputMode::String,
}
}
}

Expand All @@ -94,15 +268,8 @@ impl AsRef<Path> for OsPath {
}

impl TryFromObject for OsPath {
// TODO: path_converter with allow_fd=0 in CPython
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
let fspath = FsPath::try_from(
obj,
true,
"should be string, bytes, os.PathLike or integer",
vm,
)?;
Self::from_fspath(fspath, vm)
PathConverter::new().try_path(obj, vm)
}
}

Expand All @@ -115,15 +282,7 @@ pub(crate) enum OsPathOrFd<'fd> {

impl TryFromObject for OsPathOrFd<'_> {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
match obj.try_index_opt(vm) {
Some(int) => {
let fd = int?.try_to_primitive(vm)?;
unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }
.map(Self::Fd)
.map_err(|e| e.into_pyexception(vm))
}
None => obj.try_into_value(vm).map(Self::Path),
}
PathConverter::new().try_path_or_fd(obj, vm)
}
}

Expand Down
Loading
Loading
0