From c2fbd5068008fb039f8b75b7466e0e5858342aab Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 27 Jun 2025 16:17:57 +0900 Subject: [PATCH 1/5] Implement _stat module --- Lib/test/test_stat.py | 35 --- vm/src/stdlib/mod.rs | 2 + vm/src/stdlib/stat.rs | 571 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 573 insertions(+), 35 deletions(-) create mode 100644 vm/src/stdlib/stat.rs diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py index c1edea8491..0642eff26c 100644 --- a/Lib/test/test_stat.py +++ b/Lib/test/test_stat.py @@ -239,41 +239,6 @@ def test_file_attribute_constants(self): class TestFilemodeCStat(TestFilemode, unittest.TestCase): statmod = c_stat - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_devices(self): - super().test_devices() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_directory(self): - super().test_directory() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_file_attribute_constants(self): - super().test_file_attribute_constants() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_link(self): - super().test_link() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_mode(self): - super().test_mode() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_module_attributes(self): - super().test_module_attributes() - - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_socket(self): - super().test_socket() - class TestFilemodePyStat(TestFilemode, unittest.TestCase): statmod = py_stat diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 382a8e0555..34ee564044 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -14,6 +14,7 @@ mod operator; // TODO: maybe make this an extension module, if we ever get those // mod re; mod sre; +mod stat; mod string; #[cfg(feature = "compiler")] mod symtable; @@ -89,6 +90,7 @@ pub fn get_module_inits() -> StdlibMap { "_operator" => operator::make_module, "_signal" => signal::make_module, "_sre" => sre::make_module, + "_stat" => stat::make_module, "_string" => string::make_module, "time" => time::make_module, "_typing" => typing::make_module, diff --git a/vm/src/stdlib/stat.rs b/vm/src/stdlib/stat.rs new file mode 100644 index 0000000000..0b648bf17c --- /dev/null +++ b/vm/src/stdlib/stat.rs @@ -0,0 +1,571 @@ +use crate::{PyRef, VirtualMachine, builtins::PyModule}; + +#[pymodule] +mod stat { + #[cfg(unix)] + use libc; + + // Use libc::mode_t for Mode to match the system's definition + #[cfg(unix)] + type Mode = libc::mode_t; + #[cfg(windows)] + type Mode = u16; // Windows does not have mode_t, but stat constants are u16 + #[cfg(not(any(unix, windows)))] + type Mode = u32; // Fallback for unknown targets + + #[cfg(unix)] + #[pyattr] + pub const S_IFDIR: Mode = libc::S_IFDIR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFDIR: Mode = 0o040000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFCHR: Mode = libc::S_IFCHR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFCHR: Mode = 0o020000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFBLK: Mode = libc::S_IFBLK; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFBLK: Mode = 0o060000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFREG: Mode = libc::S_IFREG; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFREG: Mode = 0o100000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFIFO: Mode = libc::S_IFIFO; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFIFO: Mode = 0o010000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFLNK: Mode = libc::S_IFLNK; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFLNK: Mode = 0o120000; + + #[cfg(unix)] + #[pyattr] + pub const S_IFSOCK: Mode = libc::S_IFSOCK; + #[cfg(not(unix))] + #[pyattr] + pub const S_IFSOCK: Mode = 0o140000; + + // TODO: RUSTPYTHON Support Solaris + #[pyattr] + pub const S_IFDOOR: Mode = 0; + + // TODO: RUSTPYTHON Support Solaris + #[pyattr] + pub const S_IFPORT: Mode = 0; + + // TODO: RUSTPYTHON Support BSD + // https://man.freebsd.org/cgi/man.cgi?stat(2) + #[pyattr] + pub const S_IFWHT: Mode = 0; + + // Permission bits + #[cfg(unix)] + #[pyattr] + pub const S_ISUID: Mode = libc::S_ISUID; + #[cfg(not(unix))] + #[pyattr] + pub const S_ISUID: Mode = 0o4000; + + #[cfg(unix)] + #[pyattr] + pub const S_ISGID: Mode = libc::S_ISGID; + #[cfg(not(unix))] + #[pyattr] + pub const S_ISGID: Mode = 0o2000; + + #[cfg(unix)] + #[pyattr] + pub const S_ENFMT: Mode = libc::S_ISGID; + #[cfg(not(unix))] + #[pyattr] + pub const S_ENFMT: Mode = 0o2000; + + #[cfg(unix)] + #[pyattr] + pub const S_ISVTX: Mode = libc::S_ISVTX; + #[cfg(not(unix))] + #[pyattr] + pub const S_ISVTX: Mode = 0o1000; + + #[cfg(unix)] + #[pyattr] + pub const S_IRWXU: Mode = libc::S_IRWXU; + #[cfg(not(unix))] + #[pyattr] + pub const S_IRWXU: Mode = 0o0700; + + #[cfg(unix)] + #[pyattr] + pub const S_IRUSR: Mode = libc::S_IRUSR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IRUSR: Mode = 0o0400; + + #[cfg(unix)] + #[pyattr] + pub const S_IREAD: Mode = libc::S_IRUSR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IREAD: Mode = 0o0400; + + #[cfg(unix)] + #[pyattr] + pub const S_IWUSR: Mode = libc::S_IWUSR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IWUSR: Mode = 0o0200; + + #[cfg(all(unix, not(target_os = "android")))] + #[pyattr] + pub const S_IWRITE: Mode = libc::S_IWRITE; + #[cfg(any(not(unix), target_os = "android"))] + #[pyattr] + pub const S_IWRITE: Mode = 0o0200; + + #[cfg(unix)] + #[pyattr] + pub const S_IXUSR: Mode = libc::S_IXUSR; + #[cfg(not(unix))] + #[pyattr] + pub const S_IXUSR: Mode = 0o0100; + + #[cfg(all(unix, not(target_os = "android")))] + #[pyattr] + pub const S_IEXEC: Mode = libc::S_IEXEC; + #[cfg(any(not(unix), target_os = "android"))] + #[pyattr] + pub const S_IEXEC: Mode = 0o0100; + + #[cfg(unix)] + #[pyattr] + pub const S_IRWXG: Mode = libc::S_IRWXG; + #[cfg(not(unix))] + #[pyattr] + pub const S_IRWXG: Mode = 0o0070; + + #[cfg(unix)] + #[pyattr] + pub const S_IRGRP: Mode = libc::S_IRGRP; + #[cfg(not(unix))] + #[pyattr] + pub const S_IRGRP: Mode = 0o0040; + + #[cfg(unix)] + #[pyattr] + pub const S_IWGRP: Mode = libc::S_IWGRP; + #[cfg(not(unix))] + #[pyattr] + pub const S_IWGRP: Mode = 0o0020; + + #[cfg(unix)] + #[pyattr] + pub const S_IXGRP: Mode = libc::S_IXGRP; + #[cfg(not(unix))] + #[pyattr] + pub const S_IXGRP: Mode = 0o0010; + + #[cfg(unix)] + #[pyattr] + pub const S_IRWXO: Mode = libc::S_IRWXO; + #[cfg(not(unix))] + #[pyattr] + pub const S_IRWXO: Mode = 0o0007; + + #[cfg(unix)] + #[pyattr] + pub const S_IROTH: Mode = libc::S_IROTH; + #[cfg(not(unix))] + #[pyattr] + pub const S_IROTH: Mode = 0o0004; + + #[cfg(unix)] + #[pyattr] + pub const S_IWOTH: Mode = libc::S_IWOTH; + #[cfg(not(unix))] + #[pyattr] + pub const S_IWOTH: Mode = 0o0002; + + #[cfg(unix)] + #[pyattr] + pub const S_IXOTH: Mode = libc::S_IXOTH; + #[cfg(not(unix))] + #[pyattr] + pub const S_IXOTH: Mode = 0o0001; + + // Stat result indices + #[pyattr] + pub const ST_MODE: u32 = 0; + + #[pyattr] + pub const ST_INO: u32 = 1; + + #[pyattr] + pub const ST_DEV: u32 = 2; + + #[pyattr] + pub const ST_NLINK: u32 = 3; + + #[pyattr] + pub const ST_UID: u32 = 4; + + #[pyattr] + pub const ST_GID: u32 = 5; + + #[pyattr] + pub const ST_SIZE: u32 = 6; + + #[pyattr] + pub const ST_ATIME: u32 = 7; + + #[pyattr] + pub const ST_MTIME: u32 = 8; + + #[pyattr] + pub const ST_CTIME: u32 = 9; + + const S_IFMT: Mode = 0o170000; + + const S_IMODE: Mode = 0o7777; + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISDIR(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFDIR + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISCHR(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFCHR + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISREG(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFREG + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISBLK(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFBLK + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISFIFO(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFIFO + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISLNK(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFLNK + } + + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISSOCK(mode: Mode) -> bool { + (mode & S_IFMT) == S_IFSOCK + } + + // TODO: RUSTPYTHON Support Solaris + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISDOOR(_mode: Mode) -> bool { + false + } + + // TODO: RUSTPYTHON Support Solaris + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISPORT(_mode: Mode) -> bool { + false + } + + // TODO: RUSTPYTHON Support BSD + #[pyfunction] + #[allow(non_snake_case)] + fn S_ISWHT(_mode: Mode) -> bool { + false + } + + #[pyfunction(name = "S_IMODE")] + #[allow(non_snake_case)] + fn S_IMODE_method(mode: Mode) -> Mode { + mode & S_IMODE + } + + #[pyfunction(name = "S_IFMT")] + #[allow(non_snake_case)] + fn S_IFMT_method(mode: Mode) -> Mode { + // 0o170000 is from the S_IFMT definition in CPython include/fileutils.h + mode & S_IFMT + } + + #[pyfunction] + fn filetype(mode: Mode) -> char { + if S_ISREG(mode) { + '-' + } else if S_ISDIR(mode) { + 'd' + } else if S_ISLNK(mode) { + 'l' + } else if S_ISBLK(mode) { + 'b' + } else if S_ISCHR(mode) { + 'c' + } else if S_ISFIFO(mode) { + 'p' + } else if S_ISSOCK(mode) { + 's' + } else if S_ISDOOR(mode) { + 'D' // TODO: RUSTPYTHON Support Solaris + } else if S_ISPORT(mode) { + 'P' // TODO: RUSTPYTHON Support Solaris + } else if S_ISWHT(mode) { + 'w' // TODO: RUSTPYTHON Support BSD + } else { + '?' // Unknown file type + } + } + + // Convert file mode to string representation + #[pyfunction] + fn filemode(mode: Mode) -> String { + let mut result = String::with_capacity(9); + + // File type + result.push(filetype(mode)); + + // User permissions + result.push(if mode & S_IRUSR != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWUSR != 0 { 'w' } else { '-' }); + if mode & S_ISUID != 0 { + result.push(if mode & S_IXUSR != 0 { 's' } else { 'S' }); + } else { + result.push(if mode & S_IXUSR != 0 { 'x' } else { '-' }); + } + + // Group permissions + result.push(if mode & S_IRGRP != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWGRP != 0 { 'w' } else { '-' }); + if mode & S_ISGID != 0 { + result.push(if mode & S_IXGRP != 0 { 's' } else { 'S' }); + } else { + result.push(if mode & S_IXGRP != 0 { 'x' } else { '-' }); + } + + // Other permissions + result.push(if mode & S_IROTH != 0 { 'r' } else { '-' }); + result.push(if mode & S_IWOTH != 0 { 'w' } else { '-' }); + if mode & S_ISVTX != 0 { + result.push(if mode & S_IXOTH != 0 { 't' } else { 'T' }); + } else { + result.push(if mode & S_IXOTH != 0 { 'x' } else { '-' }); + } + + result + } + + // Windows file attributes (if on Windows) + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_ARCHIVE: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_ARCHIVE; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_COMPRESSED: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_COMPRESSED; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_DEVICE: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DEVICE; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_DIRECTORY: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_ENCRYPTED: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_ENCRYPTED; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_HIDDEN: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_HIDDEN; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_INTEGRITY_STREAM: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_INTEGRITY_STREAM; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_NORMAL: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NOT_CONTENT_INDEXED; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_NO_SCRUB_DATA: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NO_SCRUB_DATA; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_OFFLINE: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_OFFLINE; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_READONLY: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_READONLY; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_REPARSE_POINT: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_SPARSE_FILE: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SPARSE_FILE; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_SYSTEM: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_SYSTEM; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_TEMPORARY: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_TEMPORARY; + + #[cfg(windows)] + #[pyattr] + pub const FILE_ATTRIBUTE_VIRTUAL: u32 = + windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_VIRTUAL; + + // Unix file flags (if on Unix) + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_NODUMP: u32 = libc::UF_NODUMP; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_NODUMP: u32 = 0x00000001; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_IMMUTABLE: u32 = libc::UF_IMMUTABLE; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_IMMUTABLE: u32 = 0x00000002; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_APPEND: u32 = libc::UF_APPEND; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_APPEND: u32 = 0x00000004; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_OPAQUE: u32 = libc::UF_OPAQUE; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_OPAQUE: u32 = 0x00000008; + + #[pyattr] + pub const UF_NOUNLINK: u32 = 0x00000010; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_COMPRESSED: u32 = libc::UF_COMPRESSED; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_COMPRESSED: u32 = 0x00000020; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const UF_HIDDEN: u32 = libc::UF_HIDDEN; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const UF_HIDDEN: u32 = 0x00008000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_ARCHIVED: u32 = libc::SF_ARCHIVED; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const SF_ARCHIVED: u32 = 0x00010000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_IMMUTABLE: u32 = libc::SF_IMMUTABLE; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const SF_IMMUTABLE: u32 = 0x00020000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_APPEND: u32 = libc::SF_APPEND; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const SF_APPEND: u32 = 0x00040000; + + #[pyattr] + pub const SF_NOUNLINK: u32 = 0x00100000; + + #[pyattr] + pub const SF_SNAPSHOT: u32 = 0x00200000; + + #[pyattr] + pub const SF_FIRMLINK: u32 = 0x00800000; + + #[pyattr] + pub const SF_DATALESS: u32 = 0x40000000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_SUPPORTED: u32 = 0x009f0000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_SETTABLE: u32 = 0x3fff0000; + #[cfg(not(target_os = "macos"))] + #[pyattr] + pub const SF_SETTABLE: u32 = 0xffff0000; + + #[cfg(target_os = "macos")] + #[pyattr] + pub const SF_SYNTHETIC: u32 = 0xc0000000; +} + +pub fn make_module(vm: &VirtualMachine) -> PyRef { + stat::make_module(vm) +} From fc9c02436f61c50c664b55fe67a12aa9c359dc00 Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 27 Jun 2025 22:14:00 +0900 Subject: [PATCH 2/5] Allow 'FIRMLINK' term --- .cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cspell.json b/.cspell.json index 00062d1f79..bd4809c43f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -133,6 +133,8 @@ // win32 "birthtime", "IFEXEC", + // "stat" + "FIRMLINK", ], // flagWords - list of words to be always considered incorrect "flagWords": [ From 14de13041bc57b61042950446410064bddd4cf81 Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 27 Jun 2025 23:46:40 +0900 Subject: [PATCH 3/5] Fix incorrect capacity --- vm/src/stdlib/stat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/src/stdlib/stat.rs b/vm/src/stdlib/stat.rs index 0b648bf17c..acb1554396 100644 --- a/vm/src/stdlib/stat.rs +++ b/vm/src/stdlib/stat.rs @@ -350,7 +350,7 @@ mod stat { // Convert file mode to string representation #[pyfunction] fn filemode(mode: Mode) -> String { - let mut result = String::with_capacity(9); + let mut result = String::with_capacity(10); // File type result.push(filetype(mode)); From 87eaf47a90ae58294fba35806f4d564c9ff4dc37 Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Fri, 27 Jun 2025 23:47:18 +0900 Subject: [PATCH 4/5] Remove trailing comma in JSON --- .cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index bd4809c43f..62824d2d52 100644 --- a/.cspell.json +++ b/.cspell.json @@ -134,7 +134,7 @@ "birthtime", "IFEXEC", // "stat" - "FIRMLINK", + "FIRMLINK" ], // flagWords - list of words to be always considered incorrect "flagWords": [ From 9a7507f5e1b8f26530a76da6cfa535af93cd2a24 Mon Sep 17 00:00:00 2001 From: Lee Dogeon Date: Sat, 28 Jun 2025 00:54:12 +0900 Subject: [PATCH 5/5] Mark expectedFailure for windows --- Lib/test/test_stat.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_stat.py b/Lib/test/test_stat.py index 0642eff26c..b6e9c24a80 100644 --- a/Lib/test/test_stat.py +++ b/Lib/test/test_stat.py @@ -239,6 +239,11 @@ def test_file_attribute_constants(self): class TestFilemodeCStat(TestFilemode, unittest.TestCase): statmod = c_stat + # TODO: RUSTPYTHON + if sys.platform == "win32": + @unittest.expectedFailure + def test_link(self): + super().test_link() class TestFilemodePyStat(TestFilemode, unittest.TestCase): statmod = py_stat