Skip to content

Commit 7ebb0f0

Browse files
authored
impl path_converter and os functions (RustPython#6484)
* os.setpgrp * tcgetpgrp * impl more os functions * impl PathConverter
1 parent 72cf6c3 commit 7ebb0f0

6 files changed

Lines changed: 272 additions & 58 deletions

File tree

Lib/test/test_os.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,15 +1725,15 @@ def walk(self, top, **kwargs):
17251725
bdirs[:] = list(map(os.fsencode, dirs))
17261726
bfiles[:] = list(map(os.fsencode, files))
17271727

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

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

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

@@ -4502,7 +4502,6 @@ class Str(str):
45024502

45034503
self.filenames = self.bytes_filenames + self.unicode_filenames
45044504

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

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

5340-
@unittest.expectedFailure # TODO: RUSTPYTHON
53415338
def test_fspath_set_to_None(self):
53425339
class Foo:
53435340
__fspath__ = None

crates/vm/src/function/fspath.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
};
88
use std::{borrow::Cow, ffi::OsStr, path::PathBuf};
99

10+
/// Helper to implement os.fspath()
1011
#[derive(Clone)]
1112
pub enum FsPath {
1213
Str(PyStrRef),
@@ -27,7 +28,7 @@ impl FsPath {
2728
)
2829
}
2930

30-
// PyOS_FSPath in CPython
31+
// PyOS_FSPath
3132
pub fn try_from(
3233
obj: PyObjectRef,
3334
check_for_nul: bool,

crates/vm/src/ospath.rs

Lines changed: 188 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,181 @@ use rustpython_common::crt_fd;
22

33
use crate::{
44
PyObjectRef, PyResult, VirtualMachine,
5+
builtins::{PyBytes, PyStr},
56
convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject},
67
function::FsPath,
78
};
89
use std::path::{Path, PathBuf};
910

10-
// path_ without allow_fd in CPython
11+
/// path_converter
12+
#[derive(Clone, Copy, Default)]
13+
pub struct PathConverter {
14+
/// Function name for error messages (e.g., "rename")
15+
pub function_name: Option<&'static str>,
16+
/// Argument name for error messages (e.g., "src", "dst")
17+
pub argument_name: Option<&'static str>,
18+
/// If true, embedded null characters are allowed
19+
pub non_strict: bool,
20+
}
21+
22+
impl PathConverter {
23+
pub const fn new() -> Self {
24+
Self {
25+
function_name: None,
26+
argument_name: None,
27+
non_strict: false,
28+
}
29+
}
30+
31+
pub const fn function(mut self, name: &'static str) -> Self {
32+
self.function_name = Some(name);
33+
self
34+
}
35+
36+
pub const fn argument(mut self, name: &'static str) -> Self {
37+
self.argument_name = Some(name);
38+
self
39+
}
40+
41+
pub const fn non_strict(mut self) -> Self {
42+
self.non_strict = true;
43+
self
44+
}
45+
46+
/// Generate error message prefix like "rename: "
47+
fn error_prefix(&self) -> String {
48+
match self.function_name {
49+
Some(func) => format!("{}: ", func),
50+
None => String::new(),
51+
}
52+
}
53+
54+
/// Get argument name for error messages, defaults to "path"
55+
fn arg_name(&self) -> &'static str {
56+
self.argument_name.unwrap_or("path")
57+
}
58+
59+
/// Format a type error message
60+
fn type_error_msg(&self, type_name: &str, allow_fd: bool) -> String {
61+
let expected = if allow_fd {
62+
"string, bytes, os.PathLike or integer"
63+
} else {
64+
"string, bytes or os.PathLike"
65+
};
66+
format!(
67+
"{}{} should be {}, not {}",
68+
self.error_prefix(),
69+
self.arg_name(),
70+
expected,
71+
type_name
72+
)
73+
}
74+
75+
/// Convert to OsPathOrFd (path or file descriptor)
76+
pub(crate) fn try_path_or_fd<'fd>(
77+
&self,
78+
obj: PyObjectRef,
79+
vm: &VirtualMachine,
80+
) -> PyResult<OsPathOrFd<'fd>> {
81+
// Handle fd (before __fspath__ check, like CPython)
82+
if let Some(int) = obj.try_index_opt(vm) {
83+
let fd = int?.try_to_primitive(vm)?;
84+
return unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }
85+
.map(OsPathOrFd::Fd)
86+
.map_err(|e| e.into_pyexception(vm));
87+
}
88+
89+
self.try_path_inner(obj, true, vm).map(OsPathOrFd::Path)
90+
}
91+
92+
/// Convert to OsPath only (no fd support)
93+
fn try_path_inner(
94+
&self,
95+
obj: PyObjectRef,
96+
allow_fd: bool,
97+
vm: &VirtualMachine,
98+
) -> PyResult<OsPath> {
99+
// Try direct str/bytes match
100+
let obj = match self.try_match_str_bytes(obj.clone(), vm)? {
101+
Ok(path) => return Ok(path),
102+
Err(obj) => obj,
103+
};
104+
105+
// Call __fspath__
106+
let type_error_msg = || self.type_error_msg(&obj.class().name(), allow_fd);
107+
let method =
108+
vm.get_method_or_type_error(obj.clone(), identifier!(vm, __fspath__), type_error_msg)?;
109+
if vm.is_none(&method) {
110+
return Err(vm.new_type_error(type_error_msg()));
111+
}
112+
let result = method.call((), vm)?;
113+
114+
// Match __fspath__ result
115+
self.try_match_str_bytes(result.clone(), vm)?.map_err(|_| {
116+
vm.new_type_error(format!(
117+
"{}expected {}.__fspath__() to return str or bytes, not {}",
118+
self.error_prefix(),
119+
obj.class().name(),
120+
result.class().name(),
121+
))
122+
})
123+
}
124+
125+
/// Try to match str or bytes, returns Err(obj) if neither
126+
fn try_match_str_bytes(
127+
&self,
128+
obj: PyObjectRef,
129+
vm: &VirtualMachine,
130+
) -> PyResult<Result<OsPath, PyObjectRef>> {
131+
let check_nul = |b: &[u8]| {
132+
if self.non_strict || memchr::memchr(b'\0', b).is_none() {
133+
Ok(())
134+
} else {
135+
Err(vm.new_value_error(format!(
136+
"{}embedded null character in {}",
137+
self.error_prefix(),
138+
self.arg_name()
139+
)))
140+
}
141+
};
142+
143+
match_class!(match obj {
144+
s @ PyStr => {
145+
check_nul(s.as_bytes())?;
146+
let path = vm.fsencode(&s)?.into_owned();
147+
Ok(Ok(OsPath {
148+
path,
149+
origin: Some(s.into()),
150+
}))
151+
}
152+
b @ PyBytes => {
153+
check_nul(&b)?;
154+
let path = FsPath::bytes_as_os_str(&b, vm)?.to_owned();
155+
Ok(Ok(OsPath {
156+
path,
157+
origin: Some(b.into()),
158+
}))
159+
}
160+
obj => Ok(Err(obj)),
161+
})
162+
}
163+
164+
/// Convert to OsPath directly
165+
pub fn try_path(&self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<OsPath> {
166+
self.try_path_inner(obj, false, vm)
167+
}
168+
}
169+
170+
/// path_t output - the converted path
11171
#[derive(Clone)]
12172
pub struct OsPath {
13173
pub path: std::ffi::OsString,
14-
pub(super) mode: OutputMode,
174+
/// Original Python object for identity preservation in OSError
175+
pub(super) origin: Option<PyObjectRef>,
15176
}
16177

17178
#[derive(Debug, Copy, Clone)]
18-
pub(super) enum OutputMode {
179+
pub enum OutputMode {
19180
String,
20181
Bytes,
21182
}
@@ -38,19 +199,19 @@ impl OutputMode {
38199
impl OsPath {
39200
pub fn new_str(path: impl Into<std::ffi::OsString>) -> Self {
40201
let path = path.into();
41-
Self {
42-
path,
43-
mode: OutputMode::String,
44-
}
202+
Self { path, origin: None }
45203
}
46204

47205
pub(crate) fn from_fspath(fspath: FsPath, vm: &VirtualMachine) -> PyResult<Self> {
48206
let path = fspath.as_os_str(vm)?.into_owned();
49-
let mode = match fspath {
50-
FsPath::Str(_) => OutputMode::String,
51-
FsPath::Bytes(_) => OutputMode::Bytes,
207+
let origin = match fspath {
208+
FsPath::Str(s) => s.into(),
209+
FsPath::Bytes(b) => b.into(),
52210
};
53-
Ok(Self { path, mode })
211+
Ok(Self {
212+
path,
213+
origin: Some(origin),
214+
})
54215
}
55216

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

85246
pub fn filename(&self, vm: &VirtualMachine) -> PyObjectRef {
86-
self.mode.process_path(self.path.clone(), vm)
247+
if let Some(ref origin) = self.origin {
248+
origin.clone()
249+
} else {
250+
// Default to string when no origin (e.g., from new_str)
251+
OutputMode::String.process_path(self.path.clone(), vm)
252+
}
253+
}
254+
255+
/// Get the output mode based on origin type (bytes -> Bytes, otherwise -> String)
256+
pub fn mode(&self) -> OutputMode {
257+
match &self.origin {
258+
Some(obj) if obj.downcast_ref::<PyBytes>().is_some() => OutputMode::Bytes,
259+
_ => OutputMode::String,
260+
}
87261
}
88262
}
89263

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

96270
impl TryFromObject for OsPath {
97-
// TODO: path_converter with allow_fd=0 in CPython
98271
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
99-
let fspath = FsPath::try_from(
100-
obj,
101-
true,
102-
"should be string, bytes, os.PathLike or integer",
103-
vm,
104-
)?;
105-
Self::from_fspath(fspath, vm)
272+
PathConverter::new().try_path(obj, vm)
106273
}
107274
}
108275

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

116283
impl TryFromObject for OsPathOrFd<'_> {
117284
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
118-
match obj.try_index_opt(vm) {
119-
Some(int) => {
120-
let fd = int?.try_to_primitive(vm)?;
121-
unsafe { crt_fd::Borrowed::try_borrow_raw(fd) }
122-
.map(Self::Fd)
123-
.map_err(|e| e.into_pyexception(vm))
124-
}
125-
None => obj.try_into_value(vm).map(Self::Path),
126-
}
285+
PathConverter::new().try_path_or_fd(obj, vm)
127286
}
128287
}
129288

0 commit comments

Comments
 (0)