Align codec, call, exception, and Unix module behavior with CPython (#8647)

* Collect consecutive charmap encoding failures

Pass one contiguous run of unmappable characters to the codec error handler, and use the full run when a text replacement is itself unmappable.

Assisted-by: Codex:gpt-5

* Leave conversions out of standalone format specs

A conversion belongs to a replacement field rather than the format specification consumed by format(), so stop accepting and storing it in FormatSpec.

Assisted-by: Codex:gpt-5

* Decode empty byte strings without looking up a codec

Match PyUnicode_FromEncodedObject by returning the shared empty string before resolving the requested encoding for empty bytes and bytearray inputs.

Assisted-by: Codex:gpt-5

* Validate call shape before converting builtin arguments

Reject unknown keywords and excess arguments before conversions can raise a different error. Align Python-function positional-only handling, use declared list arguments in heapq, keep delegated codecs positional-only, and preserve CPython parameter names and validation order.

Assisted-by: Codex:gpt-5

* Align attribute and exception initialization errors

Give __dict__ and type __qualname__ their CPython diagnostics, preserve OSError filename fields correctly, accept seven-item SyntaxError details, and report Unicode error constructor arity before converting values.

Assisted-by: Codex:gpt-5

* Expose missing Unix and Darwin module APIs

Add the platform constants exported by fcntl, mmap, select, socket, syslog, termios, stat, errno, time, and _sre. Implement posix.killpg and resource.getpagesize, and expose resource.error as OSError.

Assisted-by: Codex:gpt-5

* Honor the configured Python CPU count

Parse -X cpu_count and PYTHON_CPU_COUNT, expose the stored value through sys._get_cpu_count_config, and let os.cpu_count prefer it over the host count. Also publish getsizeof with its CPython parameter name.

Assisted-by: Codex:gpt-5

* Fix CPU count and WASI CI coverage

Remove the obsolete expected-failure marker now that CPU count overrides pass, and avoid exporting FASYNC where WASI libc has no O_ASYNC constant.

Assisted-by: Codex:gpt-5

* self review
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-05 11:34:43 +09:00
1 parent 9b5e550f51
commit 699913d8cd
38 files changed
+593 -331

No files matched your search

+1
View File
@@ -293,3 +293,4 @@ XSETREF
xibufferview
xstat
XXPRIME
ENOTCAPABLE
+37 -22
View File
@@ -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 <undefined>",
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 <undefined>"
)
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 <undefined>",
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 <undefined>"
)
inpos += 1
inpos = charmap_encoding_error(p, size, inpos, mapping, errors, res)
else:
res += x
inpos += 1
return res
-1
View File
@@ -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)
-1
View File
@@ -2274,7 +2274,6 @@ class TestGetcallargsFunctions(unittest.TestCase):
'(4,[5,6])]), q=0, **collections.UserDict('
'y=9, z=10)')
@unittest.expectedFailure # TODO: RUSTPYTHON; + <lambda>() got an unexpected keyword argument 'x'
def test_errors(self):
f0 = self.makeCallable('')
f1 = self.makeCallable('a, b')
-1
View File
@@ -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)
+1 -31
View File
@@ -39,18 +39,6 @@ pub enum FormatConversion {
Ascii = b'b',
}
impl FormatParse for FormatConversion {
fn parse(text: &Wtf8) -> (Option<Self>, &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<Self> {
@@ -61,15 +49,6 @@ impl FormatConversion {
_ => None,
}
}
fn from_string(text: &Wtf8) -> Option<Self> {
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<FormatConversion>,
fill: Option<CodePoint>,
align: Option<FormatAlign>,
align_specified: bool,
@@ -351,8 +329,6 @@ impl FormatSpec {
}
fn _parse(text: &Wtf8) -> Result<Self, FormatSpecError> {
// 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,
+20 -1
View File
@@ -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,
+76 -8
View File
@@ -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};
+14
View File
@@ -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)
+4 -1
View File
@@ -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::{
+3
View File
@@ -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<CStr>),
+6
View File
@@ -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",
+25 -97
View File
@@ -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::<PyList>().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<PyObjectRef> {
let lst = heap.downcast::<PyList>().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<PyObjectRef> {
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<PyObjectRef> {
let lst = heap.downcast::<PyList>().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<PyObjectRef> {
let lst = heap.downcast::<PyList>().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::<PyList>().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::<PyList>().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<PyObjectRef> {
let lst = heap.downcast::<PyList>().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<PyObjectRef> {
heappop_internal(&heap, siftup_max, vm)
}
#[pyfunction]
fn heapreplace_max(
heap: PyObjectRef,
heap: PyListRef,
item: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<PyObjectRef> {
let lst = heap.downcast::<PyList>().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::<PyList>().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<PyObjectRef> {
let lst = heap.downcast::<PyList>().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)
}
}
+9 -1
View File
@@ -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::{
+9
View File
@@ -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]
+12
View File
@@ -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<RUsageData> {
let res = host_resource::getrusage(who);
+4 -1
View File
@@ -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 {
+8
View File
@@ -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::{
+4
View File
@@ -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<PyStrRef> {
if let Some(argv) = vm.state.config.settings.argv.first()
&& !argv.is_empty()
+6
View File
@@ -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,
+70 -57
View File
@@ -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,
)));
}
+18 -15
View File
@@ -510,35 +510,38 @@ pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDict
}
pub(crate) fn object_set_dict(
obj: PyObjectRef,
value: PySetterValue<PyDictRef>,
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<PyDictRef> {
value.downcast::<PyDict>().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::<PyDict>().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) {
+1 -1
View File
@@ -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)?;
+3 -5
View File
@@ -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<PyRef<PyStr>> {
match value.downcast::<PyStr>() {
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(())
}
+1 -1
View File
@@ -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)]
+25 -2
View File
@@ -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<PyUtf8StrRef>,
vm: &VirtualMachine,
) -> PyResult<PyStrRef> {
let empty = if let Some(bytes) = obj.downcast_ref::<PyBytes>() {
bytes.as_bytes().is_empty()
} else if let Some(bytes) = obj.downcast_ref::<PyByteArray>() {
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,
+36 -9
View File
@@ -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,
+29 -29
View File
@@ -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<crate::PyObjectRef>,
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)]
+8 -2
View File
@@ -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)]
+76 -39
View File
@@ -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<Self> {
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
+8 -2
View File
@@ -1217,7 +1217,7 @@ mod builtins {
#[derive(FromArgs)]
struct ImportArgs {
#[pyarg(any)]
name: PyStrRef,
name: PyObjectRef,
#[pyarg(any, default)]
globals: Option<PyObjectRef>,
#[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::<PyStr>()
.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]
+2
View File
@@ -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",
+5
View File
@@ -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()
}
+5
View File
@@ -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<i32>,
+8 -2
View File
@@ -953,7 +953,7 @@ pub mod sys {
#[derive(FromArgs)]
struct GetsizeofArgs {
obj: PyObjectRef,
object: PyObjectRef,
#[pyarg(any, optional)]
default: Option<PyObjectRef>,
}
@@ -961,7 +961,7 @@ pub mod sys {
#[pyfunction]
fn getsizeof(args: GetsizeofArgs, vm: &VirtualMachine) -> PyResult {
let sizeof = || -> PyResult<usize> {
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::<usize>(vm)?;
Ok(res + core::mem::size_of::<PyObject>())
};
@@ -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()
+7 -1
View File
@@ -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};
+5
View File
@@ -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<NonZeroI32>,
// /* --- 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")]
+47 -1
View File
@@ -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<Option<NonZeroI32>, ()> {
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<Item = String> + '_ {
env::var_os(env_variable_name)
.filter(|v| !v.is_empty())