diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 8ef440d5e..60c2322e0 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -293,3 +293,4 @@ XSETREF xibufferview xstat XXPRIME +ENOTCAPABLE diff --git a/Lib/_pycodecs.py b/Lib/_pycodecs.py index b6065fa01..028865791 100644 --- a/Lib/_pycodecs.py +++ b/Lib/_pycodecs.py @@ -1687,6 +1687,39 @@ def charmapencode_output(c, mapping): raise TypeError("character mapping must return integer, None or str") +def charmap_encoding_error(p, size, inpos, mapping, errors, res): + # /* find all unencodable characters */ + collendpos = inpos + 1 + while collendpos < size: + try: + charmapencode_output(ord(p[collendpos]), mapping) + except KeyError: + collendpos += 1 + else: + break + + replacement, newpos = unicode_call_errorhandler( + errors, + "charmap", + "character maps to ", + p, + inpos, + collendpos, + False, + ) + if isinstance(replacement, bytes): + res += list(replacement) + else: + for y in replacement: + try: + res += charmapencode_output(ord(y), mapping) + except KeyError: + raise UnicodeEncodeError( + "charmap", p, inpos, collendpos, "character maps to " + ) + return newpos + + def PyUnicode_EncodeCharmap(p, size, mapping="latin-1", errors="strict"): ## /* the following variable is used for caching string comparisons ## * -1=not initialized, 0=unknown, 1=strict, 2=replace, @@ -1703,29 +1736,11 @@ def PyUnicode_EncodeCharmap(p, size, mapping="latin-1", errors="strict"): # /* try to encode it */ try: x = charmapencode_output(ord(p[inpos]), mapping) - res += x except KeyError: - x = unicode_call_errorhandler( - errors, - "charmap", - "character maps to ", - p, - inpos, - inpos + 1, - False, - ) - replacement = x[0] - if isinstance(replacement, bytes): - res += list(replacement) - else: - try: - for y in replacement: - res += charmapencode_output(ord(y), mapping) - except KeyError: - raise UnicodeEncodeError( - "charmap", p, inpos, inpos + 1, "character maps to " - ) - inpos += 1 + inpos = charmap_encoding_error(p, size, inpos, mapping, errors, res) + else: + res += x + inpos += 1 return res diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 7bb2a1503..018ca41e2 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1192,7 +1192,6 @@ class CmdLineTest(unittest.TestCase): code = template.replace('-', '\t').replace('+', ' ') assert_python_failure('-c', code) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_cpu_count(self): code = "import os; print(os.cpu_count(), os.process_cpu_count())" res = assert_python_ok('-X', 'cpu_count=4321', '-c', code) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 9fe4051f7..7c8ded72b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -2274,7 +2274,6 @@ class TestGetcallargsFunctions(unittest.TestCase): '(4,[5,6])]), q=0, **collections.UserDict(' 'y=9, z=10)') - @unittest.expectedFailure # TODO: RUSTPYTHON; + () got an unexpected keyword argument 'x' def test_errors(self): f0 = self.makeCallable('') f1 = self.makeCallable('a, b') diff --git a/Lib/test/test_resource.py b/Lib/test/test_resource.py index 6c7145caa..41ab28d1a 100644 --- a/Lib/test/test_resource.py +++ b/Lib/test/test_resource.py @@ -197,7 +197,6 @@ class ResourceTest(unittest.TestCase): resource.setrlimit(resource.RLIMIT_CPU, BadSequence()) - @unittest.expectedFailure # TODO: RUSTPYTHON; module 'resource' has no attribute 'getpagesize' def test_pagesize(self): pagesize = resource.getpagesize() self.assertIsInstance(pagesize, int) diff --git a/crates/common/src/format.rs b/crates/common/src/format.rs index 6bbbdf2d0..4357a0c66 100644 --- a/crates/common/src/format.rs +++ b/crates/common/src/format.rs @@ -39,18 +39,6 @@ pub enum FormatConversion { Ascii = b'b', } -impl FormatParse for FormatConversion { - fn parse(text: &Wtf8) -> (Option, &Wtf8) { - let Some(conversion) = Self::from_string(text) else { - return (None, text); - }; - let mut chars = text.code_points(); - chars.next(); // Consume the bang - chars.next(); // Consume one r,s,a char - (Some(conversion), chars.as_wtf8()) - } -} - impl FormatConversion { #[must_use] pub fn from_char(c: CodePoint) -> Option { @@ -61,15 +49,6 @@ impl FormatConversion { _ => None, } } - - fn from_string(text: &Wtf8) -> Option { - let mut chars = text.code_points(); - if chars.next()? != '!' { - return None; - } - - Self::from_char(chars.next()?) - } } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -222,7 +201,6 @@ impl FormatParse for FormatType { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FormatSpec { - conversion: Option, fill: Option, align: Option, align_specified: bool, @@ -351,8 +329,6 @@ impl FormatSpec { } fn _parse(text: &Wtf8) -> Result { - // get_integer in CPython - let (conversion, text) = FormatConversion::parse(text); let (mut fill, mut align, text) = parse_fill_and_align(text); let align_specified = align.is_some(); let (sign, text) = FormatSign::parse(text); @@ -381,7 +357,6 @@ impl FormatSpec { } Ok(Self { - conversion, fill, align, align_specified, @@ -835,7 +810,6 @@ impl FormatSpec { /// parses to. Written as a destructure so a new field cannot be forgotten here. fn is_empty(&self) -> bool { let Self { - conversion, fill, align, align_specified, @@ -848,8 +822,7 @@ impl FormatSpec { frac_grouping_option, format_type, } = self; - conversion.is_none() - && fill.is_none() + fill.is_none() && align.is_none() && !align_specified && sign.is_none() @@ -1717,7 +1690,6 @@ mod tests { #[test] fn width_only() { let expected = Ok(FormatSpec { - conversion: None, fill: None, align: None, align_specified: false, @@ -1736,7 +1708,6 @@ mod tests { #[test] fn fill_and_width() { let expected = Ok(FormatSpec { - conversion: None, fill: Some('<'.into()), align: Some(FormatAlign::Right), align_specified: true, @@ -1755,7 +1726,6 @@ mod tests { #[test] fn all() { let expected = Ok(FormatSpec { - conversion: None, fill: Some('<'.into()), align: Some(FormatAlign::Right), align_specified: true, diff --git a/crates/host_env/src/fcntl.rs b/crates/host_env/src/fcntl.rs index 6fb974ba5..ee0cd58f2 100644 --- a/crates/host_env/src/fcntl.rs +++ b/crates/host_env/src/fcntl.rs @@ -19,9 +19,28 @@ pub use libc::{F_FULLFSYNC, F_NOCACHE}; #[cfg(target_os = "freebsd")] pub use libc::{F_DUP2FD, F_DUP2FD_CLOEXEC}; -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(any(target_os = "android", target_os = "linux", target_vendor = "apple"))] pub use libc::{F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW}; +/// `FASYNC` is what `fcntl.h` calls the flag `O_ASYNC` stands for. +#[cfg(not(target_os = "wasi"))] +pub const FASYNC: libc::c_int = libc::O_ASYNC; + +#[cfg(target_vendor = "apple")] +pub use libc::F_RDAHEAD; + +/// The commands `fcntl.h` carries that libc does not name. +#[cfg(target_vendor = "apple")] +mod darwin_fcntl { + pub const F_SETNOSIGPIPE: libc::c_int = 73; + pub const F_GETNOSIGPIPE: libc::c_int = 74; + pub const F_SETLEASE: libc::c_int = 106; + pub const F_GETLEASE: libc::c_int = 107; +} + +#[cfg(target_vendor = "apple")] +pub use darwin_fcntl::{F_GETLEASE, F_GETNOSIGPIPE, F_SETLEASE, F_SETNOSIGPIPE}; + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] pub use libc::{ F_ADD_SEALS, F_GET_SEALS, F_GETLEASE, F_GETPIPE_SZ, F_NOTIFY, F_SEAL_GROW, F_SEAL_SEAL, diff --git a/crates/host_env/src/mmap.rs b/crates/host_env/src/mmap.rs index c1071a716..976d78e2c 100644 --- a/crates/host_env/src/mmap.rs +++ b/crates/host_env/src/mmap.rs @@ -7,10 +7,84 @@ use std::io; #[cfg(unix)] pub use libc::{ - MADV_DONTNEED, MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL, MADV_WILLNEED, MAP_ANON, - MAP_ANONYMOUS, MAP_PRIVATE, MAP_SHARED, PROT_EXEC, PROT_READ, PROT_WRITE, + MADV_DONTNEED, MADV_NORMAL, MADV_RANDOM, MADV_SEQUENTIAL, MADV_WILLNEED, PROT_EXEC, PROT_READ, + PROT_WRITE, }; +#[cfg(unix)] +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct MapFlags: libc::c_int { + const MAP_SHARED = libc::MAP_SHARED; + const MAP_PRIVATE = libc::MAP_PRIVATE; + const MAP_ANON = libc::MAP_ANON; + const MAP_ANONYMOUS = libc::MAP_ANONYMOUS; + + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] + const MAP_DENYWRITE = libc::MAP_DENYWRITE; + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] + const MAP_EXECUTABLE = libc::MAP_EXECUTABLE; + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] + const MAP_POPULATE = libc::MAP_POPULATE; + + #[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "netbsd"))] + const MAP_STACK = libc::MAP_STACK; + + #[cfg(target_vendor = "apple")] + const MAP_NORESERVE = libc::MAP_NORESERVE; + #[cfg(target_vendor = "apple")] + const MAP_NOEXTEND = libc::MAP_NOEXTEND; + #[cfg(target_vendor = "apple")] + const MAP_HASSEMAPHORE = libc::MAP_HASSEMAPHORE; + #[cfg(target_vendor = "apple")] + const MAP_NOCACHE = libc::MAP_NOCACHE; + #[cfg(target_vendor = "apple")] + const MAP_JIT = libc::MAP_JIT; + // libc does not name these `sys/mman.h` flags. + #[cfg(target_vendor = "apple")] + const MAP_RESILIENT_CODESIGN = 0x2000; + #[cfg(target_vendor = "apple")] + const MAP_RESILIENT_MEDIA = 0x4000; + #[cfg(target_vendor = "apple")] + const MAP_32BIT = 0x8000; + #[cfg(target_vendor = "apple")] + const MAP_TRANSLATED_ALLOW_EXECUTE = 0x20000; + #[cfg(target_vendor = "apple")] + const MAP_UNIX03 = 0x40000; + #[cfg(target_vendor = "apple")] + const MAP_TPRO = 0x80000; + } +} + +#[cfg(unix)] +macro_rules! map_flag_consts { + ($($name:ident),+ $(,)?) => { + $(pub const $name: libc::c_int = MapFlags::$name.bits();)+ + }; +} + +#[cfg(unix)] +map_flag_consts! { + MAP_SHARED, MAP_PRIVATE, MAP_ANON, MAP_ANONYMOUS, +} + +#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +map_flag_consts! { + MAP_DENYWRITE, MAP_EXECUTABLE, MAP_POPULATE, +} + +#[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "netbsd"))] +map_flag_consts! { + MAP_STACK, +} + +#[cfg(target_vendor = "apple")] +map_flag_consts! { + MAP_HASSEMAPHORE, MAP_JIT, MAP_NOCACHE, MAP_NOEXTEND, MAP_NORESERVE, + MAP_RESILIENT_CODESIGN, MAP_RESILIENT_MEDIA, MAP_32BIT, + MAP_TRANSLATED_ALLOW_EXECUTE, MAP_UNIX03, MAP_TPRO, +} + #[cfg(target_os = "macos")] pub use libc::{MADV_FREE_REUSABLE, MADV_FREE_REUSE}; @@ -50,12 +124,6 @@ pub use libc::{ ))] pub use libc::MADV_SOFT_OFFLINE; -#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] -pub use libc::{MAP_DENYWRITE, MAP_EXECUTABLE, MAP_POPULATE}; - -#[cfg(any(target_os = "linux", target_os = "openbsd", target_os = "netbsd"))] -pub use libc::MAP_STACK; - #[cfg(target_os = "freebsd")] pub use libc::{MADV_AUTOSYNC, MADV_CORE, MADV_NOCORE, MADV_NOSYNC, MADV_PROTECT}; diff --git a/crates/host_env/src/posix.rs b/crates/host_env/src/posix.rs index 6d24ea8fb..557da77b4 100644 --- a/crates/host_env/src/posix.rs +++ b/crates/host_env/src/posix.rs @@ -948,6 +948,20 @@ pub fn kill(pid: i32, sig: i32) -> std::io::Result<()> { } } +/// `killpg(2)`: signal every process in a process group. +/// +/// Not `kill(-pgid, sig)`: that spelling reads a negative pid, and `kill` +/// takes its argument from Python where a caller can already pass one, so the +/// two are not interchangeable at this layer. +pub fn killpg(pgid: i32, sig: i32) -> std::io::Result<()> { + let ret = unsafe { libc::killpg(pgid, sig) }; + if ret == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + #[cfg(not(any(target_os = "wasi", target_os = "android")))] pub fn setuid(uid: u32) -> std::io::Result<()> { nix::unistd::setuid(uid_from_raw(uid)).map_err(std::io::Error::from) diff --git a/crates/host_env/src/select.rs b/crates/host_env/src/select.rs index 3191f8d97..d9e60cfe5 100644 --- a/crates/host_env/src/select.rs +++ b/crates/host_env/src/select.rs @@ -2,7 +2,10 @@ use core::mem::MaybeUninit; use std::io; #[cfg(unix)] -pub use libc::{EINTR, FD_SETSIZE, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI}; +pub use libc::{ + EINTR, FD_SETSIZE, PIPE_BUF, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI, POLLRDBAND, + POLLRDNORM, POLLWRBAND, POLLWRNORM, +}; #[cfg(any(target_os = "linux", target_os = "android", target_os = "redox"))] pub use libc::{ diff --git a/crates/host_env/src/syslog.rs b/crates/host_env/src/syslog.rs index 2ba383773..b6e3c0cb4 100644 --- a/crates/host_env/src/syslog.rs +++ b/crates/host_env/src/syslog.rs @@ -13,6 +13,9 @@ pub use libc::{ #[cfg(not(target_os = "redox"))] pub use libc::{LOG_AUTHPRIV, LOG_CRON, LOG_PERROR}; +#[cfg(target_vendor = "apple")] +pub use libc::{LOG_FTP, LOG_INSTALL, LOG_LAUNCHD, LOG_NETINFO, LOG_RAS, LOG_REMOTEAUTH}; + #[derive(Debug)] enum GlobalIdent { Explicit(Box), diff --git a/crates/host_env/src/termios.rs b/crates/host_env/src/termios.rs index 612f68be9..0ee7a124b 100644 --- a/crates/host_env/src/termios.rs +++ b/crates/host_env/src/termios.rs @@ -50,6 +50,12 @@ pub use libc::{ TIOCPKT_NOSTOP, TIOCPKT_START, TIOCPKT_STOP, }; +#[cfg(target_os = "macos")] +pub use libc::{ + _POSIX_VDISABLE, ALTWERASE, B7200, B14400, B28800, B76800, CIGNORE, EXTPROC, IUTF8, MDMBUF, + NOKERNINFO, ONOEOT, OXTABS, VDSUSP, VSTATUS, +}; + #[cfg(any( target_os = "android", target_os = "freebsd", diff --git a/crates/stdlib/src/_heapq.rs b/crates/stdlib/src/_heapq.rs index 1b416a474..670ec7632 100644 --- a/crates/stdlib/src/_heapq.rs +++ b/crates/stdlib/src/_heapq.rs @@ -5,9 +5,7 @@ pub(crate) use _heapq::module_def; mod _heapq { use crate::vm::{ - AsObject, PyObjectRef, PyResult, VirtualMachine, - builtins::{PyList, PyListRef}, - types::PyComparisonOp, + PyObjectRef, PyResult, VirtualMachine, builtins::PyListRef, types::PyComparisonOp, }; /// [CPython's siftdown](https://github.com/python/cpython/blob/v3.14.5/Modules/_heapqmodule.c#L25-L68) @@ -125,15 +123,8 @@ mod _heapq { } #[pyfunction] - fn heappush(heap: PyObjectRef, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappush() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heappush_internal(&lst, item, siftdown, vm) + fn heappush(heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + heappush_internal(&heap, item, siftdown, vm) } /// [CPython's heappop_internal](https://github.com/python/cpython/blob/v3.14.5/Modules/_heapqmodule.c#L152-L183) @@ -165,15 +156,8 @@ mod _heapq { } #[pyfunction] - fn heappop(heap: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappop() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heappop_internal(&lst, siftup, vm) + fn heappop(heap: PyListRef, vm: &VirtualMachine) -> PyResult { + heappop_internal(&heap, siftup, vm) } /// [CPython's heapreplace_internal](https://github.com/python/cpython/blob/v3.14.5/Modules/_heapqmodule.c#L202-L220) @@ -203,35 +187,21 @@ mod _heapq { #[pyfunction] fn heapreplace( - heap: PyObjectRef, + heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine, ) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heapreplace() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heapreplace_internal(&lst, item, siftup, vm) + heapreplace_internal(&heap, item, siftup, vm) } #[pyfunction] fn heappushpop( - heap: PyObjectRef, + heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine, ) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappushpop() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - let top = { - let vec = lst.borrow_vec(); + let vec = heap.borrow_vec(); match vec.first() { Some(v) => v.clone(), None => return Ok(item), @@ -244,7 +214,7 @@ mod _heapq { } let returnitem = { - let mut vec = lst.borrow_vec_mut(); + let mut vec = heap.borrow_vec_mut(); let root = match vec.first() { Some(v) => v.clone(), None => return Err(vm.new_index_error("index out of range")), @@ -254,7 +224,7 @@ mod _heapq { root }; - siftup(&lst, 0, vm)?; + siftup(&heap, 0, vm)?; Ok(returnitem) } @@ -333,15 +303,8 @@ mod _heapq { } #[pyfunction] - fn heapify(heap: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heapify() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heapify_internal(&lst, siftup, vm) + fn heapify(heap: PyListRef, vm: &VirtualMachine) -> PyResult<()> { + heapify_internal(&heap, siftup, vm) } /// [CPython's siftdown_max](https://github.com/python/cpython/blob/v3.14.5/Modules/_heapqmodule.c#L407-L449) @@ -435,72 +398,37 @@ mod _heapq { } #[pyfunction] - fn heappush_max(heap: PyObjectRef, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappush_max() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heappush_internal(&lst, item, siftdown_max, vm) + fn heappush_max(heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + heappush_internal(&heap, item, siftdown_max, vm) } #[pyfunction] - fn heappop_max(heap: PyObjectRef, vm: &VirtualMachine) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappop_max() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heappop_internal(&lst, siftup_max, vm) + fn heappop_max(heap: PyListRef, vm: &VirtualMachine) -> PyResult { + heappop_internal(&heap, siftup_max, vm) } #[pyfunction] fn heapreplace_max( - heap: PyObjectRef, + heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine, ) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heapreplace_max() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heapreplace_internal(&lst, item, siftup_max, vm) + heapreplace_internal(&heap, item, siftup_max, vm) } #[pyfunction] - fn heapify_max(heap: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heapify_max() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - - heapify_internal(&lst, siftup_max, vm) + fn heapify_max(heap: PyListRef, vm: &VirtualMachine) -> PyResult<()> { + heapify_internal(&heap, siftup_max, vm) } #[pyfunction] fn heappushpop_max( - heap: PyObjectRef, + heap: PyListRef, item: PyObjectRef, vm: &VirtualMachine, ) -> PyResult { - let lst = heap.downcast::().map_err(|obj| { - vm.new_type_error(format!( - "heappushpop_max() argument 1 must be list, not {}", - obj.class().name() - )) - })?; - let top = { - let vec = lst.borrow_vec(); + let vec = heap.borrow_vec(); match vec.first() { Some(v) => v.clone(), None => return Ok(item), @@ -513,7 +441,7 @@ mod _heapq { } let returnitem = { - let mut vec = lst.borrow_vec_mut(); + let mut vec = heap.borrow_vec_mut(); let root = match vec.first() { Some(v) => v.clone(), None => return Err(vm.new_index_error("index out of range")), @@ -523,7 +451,7 @@ mod _heapq { root }; - siftup_max(&lst, 0, vm)?; + siftup_max(&heap, 0, vm)?; Ok(returnitem) } } diff --git a/crates/stdlib/src/fcntl.rs b/crates/stdlib/src/fcntl.rs index dca53c104..6bbc556a8 100644 --- a/crates/stdlib/src/fcntl.rs +++ b/crates/stdlib/src/fcntl.rs @@ -45,10 +45,18 @@ mod fcntl { #[pyattr] use host_fcntl::{F_DUP2FD, F_DUP2FD_CLOEXEC}; - #[cfg(any(target_os = "android", target_os = "linux"))] + #[cfg(any(target_os = "android", target_os = "linux", target_vendor = "apple"))] #[pyattr] use host_fcntl::{F_OFD_GETLK, F_OFD_SETLK, F_OFD_SETLKW}; + #[cfg(not(target_os = "wasi"))] + #[pyattr] + use host_fcntl::FASYNC; + + #[cfg(target_vendor = "apple")] + #[pyattr] + use host_fcntl::{F_GETLEASE, F_GETNOSIGPIPE, F_RDAHEAD, F_SETLEASE, F_SETNOSIGPIPE}; + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] #[pyattr] use host_fcntl::{ diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index 14957ad90..e181839bc 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -122,6 +122,15 @@ mod mmap { #[pyattr] use host_mmap::{MADV_AUTOSYNC, MADV_CORE, MADV_NOCORE, MADV_NOSYNC, MADV_PROTECT}; + // Darwin-specific mapping flags + #[cfg(target_vendor = "apple")] + #[pyattr] + use host_mmap::{ + MAP_32BIT, MAP_HASSEMAPHORE, MAP_JIT, MAP_NOCACHE, MAP_NOEXTEND, MAP_NORESERVE, + MAP_RESILIENT_CODESIGN, MAP_RESILIENT_MEDIA, MAP_TPRO, MAP_TRANSLATED_ALLOW_EXECUTE, + MAP_UNIX03, + }; + #[pyattr] const ACCESS_DEFAULT: u32 = AccessMode::Default as u32; #[pyattr] diff --git a/crates/stdlib/src/resource.rs b/crates/stdlib/src/resource.rs index cf7fe23d8..5da06632d 100644 --- a/crates/stdlib/src/resource.rs +++ b/crates/stdlib/src/resource.rs @@ -117,6 +117,18 @@ mod resource { } } + /// The exception the module reports its failures through, which is the one + /// every other call raises. + #[pyattr(name = "error", once)] + fn error(vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.exceptions.os_error.to_owned().into() + } + + #[pyfunction] + fn getpagesize() -> usize { + rustpython_host_env::os::page_size() + } + #[pyfunction] fn getrusage(who: i32, vm: &VirtualMachine) -> PyResult { let res = host_resource::getrusage(who); diff --git a/crates/stdlib/src/select.rs b/crates/stdlib/src/select.rs index 84ec92927..1af3f9e2a 100644 --- a/crates/stdlib/src/select.rs +++ b/crates/stdlib/src/select.rs @@ -180,7 +180,10 @@ mod decl { #[cfg(unix)] #[pyattr] - use host_select::{POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI}; + use host_select::{ + PIPE_BUF, POLLERR, POLLHUP, POLLIN, POLLNVAL, POLLOUT, POLLPRI, POLLRDBAND, POLLRDNORM, + POLLWRBAND, POLLWRNORM, + }; #[cfg(unix)] pub(super) mod poll { diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 7ad457bd1..48782704c 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -126,6 +126,14 @@ mod _socket { #[pyattr] use c::SO_SETFIB; + #[cfg(target_vendor = "apple")] + #[pyattr] + use c::{ + IP_ADD_SOURCE_MEMBERSHIP, IP_BLOCK_SOURCE, IP_DROP_SOURCE_MEMBERSHIP, IP_PKTINFO, + IP_RECVTTL, IP_UNBLOCK_SOURCE, IPPROTO_MAX, IPPROTO_SCTP, MSG_NOSIGNAL, + TCP_CONNECTION_INFO, + }; + #[cfg(target_os = "linux")] #[pyattr] use c::{ diff --git a/crates/stdlib/src/syslog.rs b/crates/stdlib/src/syslog.rs index 910175ec6..d5cd3cc0d 100644 --- a/crates/stdlib/src/syslog.rs +++ b/crates/stdlib/src/syslog.rs @@ -24,6 +24,10 @@ mod syslog { #[pyattr] use host_syslog::{LOG_AUTHPRIV, LOG_CRON, LOG_PERROR}; + #[cfg(target_vendor = "apple")] + #[pyattr] + use host_syslog::{LOG_FTP, LOG_INSTALL, LOG_LAUNCHD, LOG_NETINFO, LOG_RAS, LOG_REMOTEAUTH}; + fn get_argv(vm: &VirtualMachine) -> Option { if let Some(argv) = vm.state.config.settings.argv.first() && !argv.is_empty() diff --git a/crates/stdlib/src/termios.rs b/crates/stdlib/src/termios.rs index 67d382aa5..a84e1cb87 100644 --- a/crates/stdlib/src/termios.rs +++ b/crates/stdlib/src/termios.rs @@ -52,6 +52,12 @@ mod termios { ))] #[pyattr] use host_termios::TCSASOFT; + #[cfg(target_os = "macos")] + #[pyattr] + use host_termios::{ + _POSIX_VDISABLE, ALTWERASE, B7200, B14400, B28800, B76800, CIGNORE, EXTPROC, IUTF8, MDMBUF, + NOKERNINFO, ONOEOT, OXTABS, VDSUSP, VSTATUS, + }; #[pyattr] use host_termios::{ B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index b8975cf11..2db374980 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -268,59 +268,28 @@ impl PyFunction { let mut vararg_offset = total_args; // Pack other positional arguments in to *args: - if code.flags.contains(bytecode::CodeFlags::VARARGS) { + let too_many_positional = if code.flags.contains(bytecode::CodeFlags::VARARGS) { let vararg_value = vm.ctx.new_tuple(args_iter.collect()); fastlocals[vararg_offset] = Some(vararg_value.into()); vararg_offset += 1; + false } else { - // Check the number of positional arguments - if nargs > n_expected_args { - let n_defaults = self - .defaults_and_kwdefaults - .lock() - .0 - .as_ref() - .map_or(0, |d| d.len()); - let n_required = n_expected_args - n_defaults; - let takes_msg = if n_defaults > 0 { - format!("from {n_required} to {n_expected_args}") - } else { - n_expected_args.to_string() - }; + nargs > n_expected_args + }; - // Count keyword-only arguments that were actually provided - let kw_only_given = if code.kwonlyarg_count > 0 { - let start = code.arg_count as usize; - let end = start + code.kwonlyarg_count as usize; - code.varnames[start..end] - .iter() - .filter(|name| func_args.kwargs.contains_key(name.as_str())) - .count() - } else { - 0 - }; - - let given_msg = if kw_only_given > 0 { - format!( - "{} positional argument{} (and {} keyword-only argument{}) were", - nargs, - if nargs == 1 { "" } else { "s" }, - kw_only_given, - if kw_only_given == 1 { "" } else { "s" }, - ) - } else { - format!("{} {}", nargs, if nargs == 1 { "was" } else { "were" }) - }; - - return Err(vm.new_type_error(format!( - "{}() takes {} positional argument{} but {} given", - self.__qualname__(), - takes_msg, - if n_expected_args == 1 { "" } else { "s" }, - given_msg, - ))); - } - } + // The keyword-only parameters the call brought are counted alongside + // the positional ones it brought too many of, before the keywords + // themselves are taken out of the call. + let kw_only_given = if too_many_positional && code.kwonlyarg_count > 0 { + let start = code.arg_count as usize; + let end = start + code.kwonlyarg_count as usize; + code.varnames[start..end] + .iter() + .filter(|name| func_args.kwargs.contains_key(name.as_str())) + .count() + } else { + 0 + }; // Do we support `**kwargs` ? let kwargs = if code.flags.contains(bytecode::CodeFlags::VARKEYWORDS) { @@ -341,9 +310,9 @@ impl PyFunction { .map(|(p, _)| p) }; - let mut posonly_passed_as_kwarg = Vec::new(); // Handle keyword arguments - for (name, value) in func_args.kwargs { + let mut kwargs_iter = func_args.kwargs.into_iter(); + while let Some((name, value)) = kwargs_iter.next() { // Parameter names are plain identifiers, so a non-UTF-8 (surrogate) key // can never match one and just falls through to **kwargs / the error path. let name_str = name.as_str().ok(); @@ -362,11 +331,26 @@ impl PyFunction { *slot = Some(value); } else if let Some(kwargs) = kwargs.as_ref() { kwargs.set_item(&name, value, vm)?; - } else if name_str - .is_some_and(|s| arg_pos(0..code.posonlyarg_count as usize, s).is_some()) - { - posonly_passed_as_kwarg.push(name); } else { + // A name the call cannot place is faulted over the whole of + // what it named that only position can give, whether that + // came before this name or after it. + let is_posonly = |name: &Wtf8Buf| { + name.as_str() + .is_ok_and(|s| arg_pos(0..code.posonlyarg_count as usize, s).is_some()) + }; + let mut posonly: Vec<_> = is_posonly(&name) + .then(|| name.clone()) + .into_iter() + .collect(); + posonly.extend(kwargs_iter.map(|(name, _)| name).filter(is_posonly)); + if !posonly.is_empty() { + return Err(vm.new_type_error(format!( + "{}() got some positional-only arguments passed as keyword arguments: '{}'", + self.__qualname__(), + posonly.into_iter().format(", "), + ))); + } return Err(vm.new_type_error(format!( "{}() got an unexpected keyword argument '{}'", self.__qualname__(), @@ -374,11 +358,40 @@ impl PyFunction { ))); } } - if !posonly_passed_as_kwarg.is_empty() { + // The count of positional arguments is faulted once the keywords have + // all been placed. + if too_many_positional { + let n_defaults = self + .defaults_and_kwdefaults + .lock() + .0 + .as_ref() + .map_or(0, |d| d.len()); + let n_required = n_expected_args - n_defaults; + let (takes_msg, plural) = if n_defaults > 0 { + (format!("from {n_required} to {n_expected_args}"), true) + } else { + (n_expected_args.to_string(), n_expected_args != 1) + }; + + let given_msg = if kw_only_given > 0 { + format!( + "{} positional argument{} (and {} keyword-only argument{}) were", + nargs, + if nargs == 1 { "" } else { "s" }, + kw_only_given, + if kw_only_given == 1 { "" } else { "s" }, + ) + } else { + format!("{} {}", nargs, if nargs == 1 { "was" } else { "were" }) + }; + return Err(vm.new_type_error(format!( - "{}() got some positional-only arguments passed as keyword arguments: '{}'", + "{}() takes {} positional argument{} but {} given", self.__qualname__(), - posonly_passed_as_kwarg.into_iter().format(", "), + takes_msg, + if plural { "s" } else { "" }, + given_msg, ))); } diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 5b52de701..f93f57ed9 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -510,35 +510,38 @@ pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult, + value: PySetterValue, vm: &VirtualMachine, ) -> PyResult<()> { let dict = match value { - PySetterValue::Assign(dict) => Some(dict), + PySetterValue::Assign(value) => Some(downcast_dict(value, vm)?), PySetterValue::Delete => None, }; obj.set_dict(dict) .map_err(|_| vm.new_attribute_error("This object has no __dict__")) } +/// The dictionary an object holds its attributes in, which is one thing and +/// one thing only. = subtype_setdict +fn downcast_dict(value: PyObjectRef, vm: &VirtualMachine) -> PyResult { + value.downcast::().map_err(|value| { + vm.new_type_error(format!( + "__dict__ must be set to a dictionary, not a '{}'", + value.class().name() + )) + }) +} + +/// = PyObject_GenericSetDict pub fn object_generic_set_dict( obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine, ) -> PyResult<()> { - let dict = match value { - PySetterValue::Assign(value) => { - let dict = value.downcast::().map_err(|value| { - vm.new_type_error(format!( - "__dict__ must be set to a dictionary, not a '{}'", - value.class().name() - )) - })?; - PySetterValue::Assign(dict) - } - PySetterValue::Delete => return Err(vm.new_type_error("cannot delete __dict__")), - }; - object_set_dict(obj, dict, vm) + if matches!(value, PySetterValue::Delete) { + return Err(vm.new_type_error("cannot delete __dict__")); + } + object_set_dict(obj, value, vm) } pub(crate) fn init(ctx: &'static Context) { diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index cd455b6f8..d2af3e6d3 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -466,7 +466,7 @@ impl Constructor for PyStr { let s = vm .state .codec_registry - .decode_text(input, enc_str, errors, vm)?; + .decode_text_object(input, enc_str, errors, vm)?; Ok(Self::from(s.as_wtf8().to_owned())) } else { let s = input.str(vm)?; diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 8a0101d82..918e92c80 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -603,11 +603,13 @@ impl PyPayload for PyType { } } +/// The name a class is built under, which the namespace it is built from may +/// carry and which is faulted for not being a string. = type_new_set_attrs fn downcast_qualname(value: PyObjectRef, vm: &VirtualMachine) -> PyResult> { match value.downcast::() { Ok(value) => Ok(value), Err(value) => Err(vm.new_type_error(format!( - "can only assign string to __qualname__, not '{}'", + "type __qualname__ must be a str, not {}", value.class().name() ))), } @@ -3160,10 +3162,6 @@ fn subtype_set_dict(obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine) } } else { // _PyObject_SetDict - let value = match value { - PySetterValue::Assign(obj) => PySetterValue::Assign(obj.try_into_value(vm)?), - PySetterValue::Delete => PySetterValue::Delete, - }; object::object_set_dict(obj, value, vm)?; Ok(()) } diff --git a/crates/vm/src/bytes_inner.rs b/crates/vm/src/bytes_inner.rs index fbdebb0bd..9a1b1553c 100644 --- a/crates/vm/src/bytes_inner.rs +++ b/crates/vm/src/bytes_inner.rs @@ -1224,7 +1224,7 @@ pub(crate) fn bytes_decode( }; vm.state .codec_registry - .decode_text(zelf, encoding, errors, vm) + .decode_text_object(zelf, encoding, errors, vm) } #[derive(FromArgs)] diff --git a/crates/vm/src/codecs.rs b/crates/vm/src/codecs.rs index 073fd102f..51a987dcf 100644 --- a/crates/vm/src/codecs.rs +++ b/crates/vm/src/codecs.rs @@ -18,8 +18,8 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject, VirtualMachine, builtins::{ - PyBaseExceptionRef, PyBytes, PyBytesRef, PyStr, PyStrRef, PyTuple, PyTupleRef, PyUtf8Str, - PyUtf8StrRef, + PyBaseExceptionRef, PyByteArray, PyBytes, PyBytesRef, PyStr, PyStrRef, PyTuple, PyTupleRef, + PyUtf8Str, PyUtf8StrRef, }, convert::ToPyObject, function::{ArgBytesLike, PyMethodDef}, @@ -342,6 +342,29 @@ impl CodecsRegistry { }) } + /// The text an encoded object holds. An object holding nothing decodes to + /// the empty string without the encoding ever being looked up. + /// = PyUnicode_FromEncodedObject + pub fn decode_text_object( + &self, + obj: PyObjectRef, + encoding: &str, + errors: Option, + vm: &VirtualMachine, + ) -> PyResult { + let empty = if let Some(bytes) = obj.downcast_ref::() { + bytes.as_bytes().is_empty() + } else if let Some(bytes) = obj.downcast_ref::() { + bytes.borrow_buf().is_empty() + } else { + false + }; + if empty { + return Ok(vm.ctx.empty_str.to_owned()); + } + self.decode_text(obj, encoding, errors, vm) + } + pub fn decode_text( &self, obj: PyObjectRef, diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 58cddb61e..caefcdc3b 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -1285,6 +1285,9 @@ pub(crate) fn errno_to_exc_type(errno: i32, vm: &VirtualMachine) -> Option<&'sta errors::EINTR => Some(excs.interrupted_error), errors::EACCES => Some(excs.permission_error), errors::EPERM => Some(excs.permission_error), + // A process without the capability to reach a resource. + #[cfg(target_vendor = "apple")] + errors::ENOTCAPABLE => Some(excs.permission_error), errors::ESRCH => Some(excs.process_lookup_error), errors::ETIMEDOUT => Some(excs.timeout_error), _ => None, @@ -2222,8 +2225,10 @@ pub(super) mod types { { exc.written.store(n); set_filename = false; - // Clear filename that was set in py_new + // The count leaves neither filename taken, so both are + // put back the way `py_new` found them. let _ = unsafe { exc.filename.swap(None) }; + let _ = unsafe { exc.filename2.swap(None) }; } if set_filename { let _ = unsafe { exc.filename.swap(Some(third_arg.clone())) }; @@ -2247,19 +2252,21 @@ pub(super) mod types { new_args.args[0] = errno_obj; } } - if len == 5 { - let _ = unsafe { exc.filename2.swap(new_args.args.get(4).cloned()) }; - } } - // args are truncated to 2 for compatibility (only when 2-5 args and filename is not None) - // truncation happens inside "if (filename && filename != Py_None)" block + // A second filename, and the two arguments the rest are cut back + // to, both follow the filename itself having been taken. let has_filename = exc .filename .to_owned() .as_ref() .is_some_and(|f| !vm.is_none(f)); if (3..=5).contains(&len) && has_filename { + if let Some(filename2) = new_args.args.get(4) + && !vm.is_none(filename2) + { + let _ = unsafe { exc.filename2.swap(Some(filename2.clone())) }; + } new_args.args.truncate(2); } PyBaseException::slot_init(zelf, new_args, vm) @@ -2708,15 +2715,20 @@ pub(super) mod types { let location_tup_len = location_tuple.len(); match location_tup_len { - 4 | 6 => {} + 4 | 6 | 7 => {} 5 => { return Err(vm.new_type_error( "end_offset must be provided when end_lineno is provided", )); } - _ => { + given if given < 4 => { return Err(vm.new_type_error(format!( - "function takes exactly 4 or 6 arguments ({location_tup_len} given)" + "function takes at least 4 arguments ({given} given)" + ))); + } + given => { + return Err(vm.new_type_error(format!( + "function takes at most 7 arguments ({given} given)" ))); } } @@ -2784,6 +2796,18 @@ pub(super) mod types { #[repr(transparent)] pub struct PyValueError(PyException); + /// Check the fixed arity expected by the tuple parser before converting + /// any of the values it was given. + fn parse_tuple_arity(args: &FuncArgs, count: usize, vm: &VirtualMachine) -> PyResult<()> { + let given = args.args.len(); + if given == count { + return Ok(()); + } + Err(vm.new_type_error(format!( + "function takes exactly {count} arguments ({given} given)" + ))) + } + #[pyexception(name, base = PyValueError, ctx = "unicode_error", impl)] #[derive(Debug)] #[repr(transparent)] @@ -2830,6 +2854,7 @@ pub(super) mod types { type Args = FuncArgs; fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + parse_tuple_arity(&args, 5, vm)?; type Args = (PyStrRef, ArgBytesLike, isize, isize, PyStrRef); let (encoding, object, start, end, reason): Args = args.bind(vm)?; set_attrs!(zelf, vm, @@ -2889,6 +2914,7 @@ pub(super) mod types { type Args = FuncArgs; fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + parse_tuple_arity(&args, 5, vm)?; type Args = (PyStrRef, PyStrRef, isize, isize, PyStrRef); let (encoding, object, start, end, reason): Args = args.bind(vm)?; set_attrs!(zelf, vm, @@ -2946,6 +2972,7 @@ pub(super) mod types { type Args = FuncArgs; fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + parse_tuple_arity(&args, 4, vm)?; type Args = (PyStrRef, isize, isize, PyStrRef); let (object, start, end, reason): Args = args.bind(vm)?; set_attrs!(zelf, vm, diff --git a/crates/vm/src/stdlib/_codecs.rs b/crates/vm/src/stdlib/_codecs.rs index 5bceccd7c..7325bfcef 100644 --- a/crates/vm/src/stdlib/_codecs.rs +++ b/crates/vm/src/stdlib/_codecs.rs @@ -16,7 +16,7 @@ mod _codecs { builtins::{PyStrRef, PyUtf8StrRef}, codecs, exceptions::nul_char_error, - function::{ArgBytesLike, FuncArgs}, + function::{ArgBytesLike, PosArgs}, }; #[pyfunction] @@ -256,107 +256,107 @@ mod _codecs { } #[pyfunction] - fn readbuffer_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn readbuffer_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(readbuffer_encode, args, vm) } #[pyfunction] - fn escape_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn escape_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(escape_encode, args, vm) } #[pyfunction] - fn escape_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn escape_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(escape_decode, args, vm) } #[pyfunction] - fn unicode_escape_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn unicode_escape_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(unicode_escape_encode, args, vm) } #[pyfunction] - fn unicode_escape_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn unicode_escape_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(unicode_escape_decode, args, vm) } #[pyfunction] - fn raw_unicode_escape_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn raw_unicode_escape_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(raw_unicode_escape_encode, args, vm) } #[pyfunction] - fn raw_unicode_escape_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn raw_unicode_escape_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(raw_unicode_escape_decode, args, vm) } #[pyfunction] - fn utf_7_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_7_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_7_encode, args, vm) } #[pyfunction] - fn utf_7_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_7_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_7_decode, args, vm) } #[pyfunction] - fn utf_16_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_encode, args, vm) } #[pyfunction] - fn utf_16_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_decode, args, vm) } #[pyfunction] - fn charmap_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn charmap_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(charmap_encode, args, vm) } #[pyfunction] - fn charmap_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn charmap_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(charmap_decode, args, vm) } #[pyfunction] - fn charmap_build(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn charmap_build(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(charmap_build, args, vm) } #[pyfunction] - fn utf_16_le_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_le_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_le_encode, args, vm) } #[pyfunction] - fn utf_16_le_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_le_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_le_decode, args, vm) } #[pyfunction] - fn utf_16_be_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_be_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_be_encode, args, vm) } #[pyfunction] - fn utf_16_be_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_be_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_be_decode, args, vm) } #[pyfunction] - fn utf_16_ex_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_16_ex_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_16_ex_decode, args, vm) } #[pyfunction] - fn utf_32_ex_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_ex_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_ex_decode, args, vm) } #[pyfunction] - fn utf_32_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_encode, args, vm) } #[pyfunction] - fn utf_32_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_decode, args, vm) } #[pyfunction] - fn utf_32_le_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_le_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_le_encode, args, vm) } #[pyfunction] - fn utf_32_le_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_le_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_le_decode, args, vm) } #[pyfunction] - fn utf_32_be_encode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_be_encode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_be_encode, args, vm) } #[pyfunction] - fn utf_32_be_decode(args: FuncArgs, vm: &VirtualMachine) -> PyResult { + fn utf_32_be_decode(args: PosArgs, vm: &VirtualMachine) -> PyResult { delegate_pycodecs!(utf_32_be_decode, args, vm) } } @@ -365,14 +365,14 @@ mod _codecs { fn delegate_pycodecs( cell: &'static StaticCell, name: &'static str, - args: crate::function::FuncArgs, + args: crate::function::PosArgs, vm: &crate::VirtualMachine, ) -> crate::PyResult { let f = cell.get_or_try_init(|| { let module = vm.import("_pycodecs", 0)?; module.get_attr(name, vm) })?; - f.call(args, vm) + f.call(args.into_vec(), vm) } #[cfg(windows)] diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index c34f4572b..b0c1988f1 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -5173,10 +5173,16 @@ mod _io { ) } + #[derive(FromArgs)] + struct OpenCodeArgs { + #[pyarg(any)] + path: PyObjectRef, + } + #[pyfunction] - fn open_code(file: PyObjectRef, vm: &VirtualMachine) -> PyResult { + fn open_code(args: OpenCodeArgs, vm: &VirtualMachine) -> PyResult { // TODO: lifecycle hooks or something? - io_open(file, Some("rb"), OpenArgs::default(), vm) + io_open(args.path, Some("rb"), OpenArgs::default(), vm) } #[derive(FromArgs)] diff --git a/crates/vm/src/stdlib/_stat.rs b/crates/vm/src/stdlib/_stat.rs index f461221da..8ee1405c8 100644 --- a/crates/vm/src/stdlib/_stat.rs +++ b/crates/vm/src/stdlib/_stat.rs @@ -7,6 +7,11 @@ mod _stat { #[cfg(windows)] use rustpython_host_env::nt as host_nt; + use malachite_bigint::Sign; + use num_traits::ToPrimitive; + + use crate::{PyObjectRef, PyResult, VirtualMachine, convert::TryFromObject}; + // Use libc::mode_t for Mode to match the system's definition #[cfg(unix)] type Mode = libc::mode_t; @@ -325,9 +330,18 @@ mod _stat { 0xffff0000 }; + #[pyattr] + pub const UF_SETTABLE: u32 = 0x0000ffff; + #[pyattr] pub const UF_NOUNLINK: u32 = 0x00000010; + #[pyattr] + pub const UF_TRACKED: u32 = 0x00000040; + + #[pyattr] + pub const UF_DATAVAULT: u32 = 0x00000080; + #[pyattr] pub const SF_NOUNLINK: u32 = 0x00100000; @@ -382,88 +396,111 @@ mod _stat { #[pyattr] pub const ST_CTIME: u32 = 9; + /// The mode an argument stands for, which is read as an unsigned long and + /// then measured against the range a mode has. = mode_converter + #[derive(Copy, Clone)] + struct ModeArg(Mode); + + impl TryFromObject for ModeArg { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + let index = obj.try_index(vm)?; + let value = index.as_bigint(); + // = PyLong_AsUnsignedLong + if value.sign() == Sign::Minus { + return Err(vm.new_overflow_error("can't convert negative value to unsigned int")); + } + let value = value.to_u64().ok_or_else(|| { + vm.new_overflow_error("Python int too large to convert to C unsigned long") + })?; + Mode::try_from(value) + .map(Self) + .map_err(|_| vm.new_overflow_error("mode out of range")) + } + } + const S_IFMT: Mode = 0o170000; const S_IMODE: Mode = 0o7777; #[pyfunction] #[allow(non_snake_case)] - const fn S_ISDIR(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFDIR + const fn S_ISDIR(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFDIR } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISCHR(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFCHR + const fn S_ISCHR(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFCHR } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISREG(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFREG + const fn S_ISREG(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFREG } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISBLK(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFBLK + const fn S_ISBLK(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFBLK } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISFIFO(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFIFO + const fn S_ISFIFO(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFIFO } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISLNK(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFLNK + const fn S_ISLNK(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFLNK } #[pyfunction] #[allow(non_snake_case)] - const fn S_ISSOCK(mode: Mode) -> bool { - (mode & S_IFMT) == S_IFSOCK + const fn S_ISSOCK(mode: ModeArg) -> bool { + (mode.0 & S_IFMT) == S_IFSOCK } // TODO: RUSTPYTHON Support Solaris #[pyfunction] #[allow(non_snake_case)] - const fn S_ISDOOR(_mode: Mode) -> bool { + const fn S_ISDOOR(_mode: ModeArg) -> bool { false } // TODO: RUSTPYTHON Support Solaris #[pyfunction] #[allow(non_snake_case)] - const fn S_ISPORT(_mode: Mode) -> bool { + const fn S_ISPORT(_mode: ModeArg) -> bool { false } // TODO: RUSTPYTHON Support BSD #[pyfunction] #[allow(non_snake_case)] - const fn S_ISWHT(_mode: Mode) -> bool { + const fn S_ISWHT(_mode: ModeArg) -> bool { false } #[pyfunction(name = "S_IMODE")] #[allow(non_snake_case)] - const fn S_IMODE_method(mode: Mode) -> Mode { - mode & S_IMODE + const fn S_IMODE_method(mode: ModeArg) -> Mode { + mode.0 & S_IMODE } #[pyfunction(name = "S_IFMT")] #[allow(non_snake_case)] - const fn S_IFMT_method(mode: Mode) -> Mode { + const fn S_IFMT_method(mode: ModeArg) -> Mode { // 0o170000 is from the S_IFMT definition in CPython include/fileutils.h - mode & S_IFMT + mode.0 & S_IFMT } - #[pyfunction] - const fn filetype(mode: Mode) -> char { + /// The one character a mode's file type is written as, which the module + /// keeps to itself. + const fn filetype(mode: ModeArg) -> char { if S_ISREG(mode) { '-' } else if S_ISDIR(mode) { @@ -491,37 +528,37 @@ mod _stat { // Convert file mode to string representation #[pyfunction] - fn filemode(mode: Mode) -> String { + fn filemode(mode: ModeArg) -> String { let mut result = String::with_capacity(10); // 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' }); + result.push(if mode.0 & S_IRUSR != 0 { 'r' } else { '-' }); + result.push(if mode.0 & S_IWUSR != 0 { 'w' } else { '-' }); + if mode.0 & S_ISUID != 0 { + result.push(if mode.0 & S_IXUSR != 0 { 's' } else { 'S' }); } else { - result.push(if mode & S_IXUSR != 0 { 'x' } else { '-' }); + result.push(if mode.0 & 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' }); + result.push(if mode.0 & S_IRGRP != 0 { 'r' } else { '-' }); + result.push(if mode.0 & S_IWGRP != 0 { 'w' } else { '-' }); + if mode.0 & S_ISGID != 0 { + result.push(if mode.0 & S_IXGRP != 0 { 's' } else { 'S' }); } else { - result.push(if mode & S_IXGRP != 0 { 'x' } else { '-' }); + result.push(if mode.0 & 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' }); + result.push(if mode.0 & S_IROTH != 0 { 'r' } else { '-' }); + result.push(if mode.0 & S_IWOTH != 0 { 'w' } else { '-' }); + if mode.0 & S_ISVTX != 0 { + result.push(if mode.0 & S_IXOTH != 0 { 't' } else { 'T' }); } else { - result.push(if mode & S_IXOTH != 0 { 'x' } else { '-' }); + result.push(if mode.0 & S_IXOTH != 0 { 'x' } else { '-' }); } result diff --git a/crates/vm/src/stdlib/builtins.rs b/crates/vm/src/stdlib/builtins.rs index 2bb85bf23..80db3b564 100644 --- a/crates/vm/src/stdlib/builtins.rs +++ b/crates/vm/src/stdlib/builtins.rs @@ -1217,7 +1217,7 @@ mod builtins { #[derive(FromArgs)] struct ImportArgs { #[pyarg(any)] - name: PyStrRef, + name: PyObjectRef, #[pyarg(any, default)] globals: Option, #[allow(dead_code)] @@ -1231,7 +1231,13 @@ mod builtins { #[pyfunction] fn __import__(args: ImportArgs, vm: &VirtualMachine) -> PyResult { - crate::import::import_module_level(&args.name, args.globals, args.fromlist, args.level, vm) + // The name is faulted for not being a string by the import itself, + // ahead of the level beside it. = PyImport_ImportModuleLevelObject + let name = args + .name + .downcast_ref::() + .ok_or_else(|| vm.new_type_error("module name must be a string"))?; + crate::import::import_module_level(name, args.globals, args.fromlist, args.level, vm) } #[pyfunction] diff --git a/crates/vm/src/stdlib/errno.rs b/crates/vm/src/stdlib/errno.rs index 5ea0ad240..4b9f4a4bf 100644 --- a/crates/vm/src/stdlib/errno.rs +++ b/crates/vm/src/stdlib/errno.rs @@ -722,6 +722,8 @@ const ERROR_CODES: &[(&str, i32)] = &[ EPROGUNAVAIL ), e!(cfg(target_vendor = "apple"), EPWROFF), + e!(cfg(target_vendor = "apple"), EQFULL), + e!(cfg(target_vendor = "apple"), ENOTCAPABLE), e!( cfg(any( target_os = "dragonfly", diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index f6ab86413..e03eefb8d 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -1548,6 +1548,11 @@ pub(super) mod _os { #[pyfunction] fn cpu_count(vm: &VirtualMachine) -> PyObjectRef { + // A count the configuration names is answered with, over the one the + // host reports. = os_cpu_count_impl + if let Some(configured) = vm.state.config.settings.cpu_count { + return vm.ctx.new_int(configured.get()).into(); + } let cpu_count = crate::host_env::os::cpu_count(); vm.ctx.new_int(cpu_count).into() } diff --git a/crates/vm/src/stdlib/posix.rs b/crates/vm/src/stdlib/posix.rs index 41caab2dd..30e7333d8 100644 --- a/crates/vm/src/stdlib/posix.rs +++ b/crates/vm/src/stdlib/posix.rs @@ -1698,6 +1698,11 @@ pub mod module { rustpython_host_env::posix::kill(pid, sig as i32).map_err(|err| err.into_pyexception(vm)) } + #[pyfunction] + fn killpg(pgid: i32, sig: isize, vm: &VirtualMachine) -> PyResult<()> { + rustpython_host_env::posix::killpg(pgid, sig as i32).map_err(|err| err.into_pyexception(vm)) + } + #[pyfunction] fn get_terminal_size( fd: OptionalArg, diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index 85488656a..46784509c 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -953,7 +953,7 @@ pub mod sys { #[derive(FromArgs)] struct GetsizeofArgs { - obj: PyObjectRef, + object: PyObjectRef, #[pyarg(any, optional)] default: Option, } @@ -961,7 +961,7 @@ pub mod sys { #[pyfunction] fn getsizeof(args: GetsizeofArgs, vm: &VirtualMachine) -> PyResult { let sizeof = || -> PyResult { - let res = vm.call_special_method(&args.obj, identifier!(vm, __sizeof__), ())?; + let res = vm.call_special_method(&args.object, identifier!(vm, __sizeof__), ())?; let res = res.try_index(vm)?.try_to_primitive::(vm)?; Ok(res + core::mem::size_of::()) }; @@ -1222,6 +1222,12 @@ pub mod sys { PyIntInfo::from_data(IntInfoData::INFO, vm) } + /// Private function for getting PyConfig.cpu_count + #[pyfunction] + fn _get_cpu_count_config(vm: &VirtualMachine) -> i32 { + vm.state.config.settings.cpu_count.map_or(-1, |n| n.get()) + } + #[pyfunction] fn get_int_max_str_digits(vm: &VirtualMachine) -> usize { vm.state.int_max_str_digits.load() diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 736e09381..42f633dbe 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -938,6 +938,9 @@ mod platform { #[cfg(target_os = "solaris")] #[pyattr] use libc::CLOCK_HIGHRES; + #[cfg(any(target_os = "linux", target_vendor = "apple"))] + #[pyattr] + use libc::CLOCK_MONOTONIC_RAW; #[cfg(not(any( target_os = "illumos", target_os = "netbsd", @@ -958,9 +961,12 @@ mod platform { use libc::CLOCK_THREAD_CPUTIME_ID; #[cfg(target_os = "linux")] #[pyattr] - use libc::{CLOCK_BOOTTIME, CLOCK_MONOTONIC_RAW, CLOCK_TAI}; + use libc::{CLOCK_BOOTTIME, CLOCK_TAI}; #[pyattr] use libc::{CLOCK_MONOTONIC, CLOCK_REALTIME}; + #[cfg(target_vendor = "apple")] + #[pyattr] + use libc::{CLOCK_MONOTONIC_RAW_APPROX, CLOCK_UPTIME_RAW, CLOCK_UPTIME_RAW_APPROX}; #[cfg(any(target_os = "freebsd", target_os = "openbsd", target_os = "dragonfly"))] #[pyattr] use libc::{CLOCK_PROF, CLOCK_UPTIME}; diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 3c42ca0b6..f582e51f1 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -1,3 +1,4 @@ +use core::num::NonZeroI32; #[cfg(feature = "flame-it")] use std::ffi::OsString; @@ -138,6 +139,9 @@ pub struct Settings { /// -X int_max_str_digits pub int_max_str_digits: i64, + /// `-X cpu_count` / `PYTHON_CPU_COUNT` override; `None` leaves the count to the host + pub cpu_count: Option, + // /* --- Path configuration inputs ------------ */ // int pathconfig_warnings; // wchar_t *program_name; @@ -217,6 +221,7 @@ impl Default for Settings { stdio_errors: None, utf8_mode: -1, int_max_str_digits: 4300, + cpu_count: None, #[cfg(feature = "flame-it")] profile_output: None, #[cfg(feature = "flame-it")] diff --git a/src/settings.rs b/src/settings.rs index 2074abd97..ed443eaf6 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,6 +1,7 @@ use lexopt::Arg::*; use lexopt::ValueExt; use rustpython_vm::{Settings, vm::CheckHashPycsMode}; +use std::num::NonZeroI32; use std::str::FromStr; use std::{cmp, env}; @@ -262,6 +263,21 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }; } + if let Some(val) = get_env("PYTHON_CPU_COUNT") { + settings.cpu_count = match parse_cpu_count(val.to_str()) { + Ok(cpu_count) => cpu_count, + Err(()) => { + error!( + "Fatal Python error: config_init_cpu_count: \ + -X cpu_count=n option: n is missing or an invalid number, \ + n must be greater than 0\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + }; + } + settings.check_hash_pycs_mode = args.check_hash_based_pycs; if let Some(val) = get_env("PYTHONUTF8") @@ -308,6 +324,20 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { } "no_sig_int" => settings.install_signal_handlers = false, "no_debug_ranges" => settings.code_debug_ranges = false, + "cpu_count" => { + settings.cpu_count = match parse_cpu_count(value) { + Ok(cpu_count) => cpu_count, + Err(()) => { + error!( + "Fatal Python error: config_init_cpu_count: \ + -X cpu_count=n option: n is missing or an invalid \ + number, n must be greater than 0\n\ + Python runtime state: preinitialized" + ); + std::process::exit(1); + } + }; + } "int_max_str_digits" => { settings.int_max_str_digits = match value.unwrap().parse() { Ok(digits) if digits == 0 || digits >= 640 => digits, @@ -440,7 +470,23 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { Ok((settings, mode)) } -/// Helper function to retrieve a sequence of paths from an environment variable. +/// `-X cpu_count` / `PYTHON_CPU_COUNT`: a count above zero, or `"default"`. +/// `Ok(None)` leaves the count to the host. Zero, negatives, and values above +/// `i32::MAX` are rejected. +/// = config_init_cpu_count +fn parse_cpu_count(value: Option<&str>) -> Result, ()> { + match value { + None => Err(()), + Some("default") => Ok(None), + Some(number) => number + .parse() + .ok() + .filter(|count: &NonZeroI32| count.get() > 0) + .map(Some) + .ok_or(()), + } +} + fn get_paths(env_variable_name: &str) -> impl Iterator + '_ { env::var_os(env_variable_name) .filter(|v| !v.is_empty())