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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions crates/stdlib/src/_sqlite3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ mod _sqlite3 {
},
function::{
ArgCallable, ArgIntoBool, ArgIntoFloat, ArgIterable, ArgStrOrBytesLike, ArgStrictInt,
FsPath, FuncArgs, KwArgs, OptionalArg, PyComparisonValue, PySetterValue,
UnpackKeywords,
ArgumentError, FromArgs, FsPath, FuncArgs, KwArgs, OptionalArg, Param,
PyComparisonValue, PySetterValue, UnpackKeywords,
},
object::{Traverse, TraverseFn},
protocol::{
Expand Down Expand Up @@ -410,7 +410,7 @@ mod _sqlite3 {
cached_statements: c_int,
#[pyarg(named, default = ArgIntoBool::FALSE)]
uri: ArgIntoBool,
#[pyarg(named, default)]
#[pyarg(named, default, py_default = "sqlite3.LEGACY_TRANSACTION_CONTROL")]
autocommit: AutocommitMode,
}

Expand All @@ -432,7 +432,7 @@ mod _sqlite3 {
progress: Option<PyObjectRef>,
#[pyarg(named, default = "main")]
name: SqliteStringArg,
#[pyarg(named, default = 0.250)]
#[pyarg(named, default = 0.25)]
sleep: ArgIntoFloat,
}

Expand Down Expand Up @@ -943,8 +943,21 @@ mod _sqlite3 {
}
}

// CPython displays the Connection parameters but forwards the original call
// to a custom factory without validating or converting those parameters.
struct ConnectForwardArgs(FuncArgs);

impl FromArgs for ConnectForwardArgs {
const PARAMS: Option<&'static [Param]> = ConnectArgs::PARAMS;
const BINDING_PARAMS: Option<&'static [Param]> = FuncArgs::BINDING_PARAMS;

fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result<Self, ArgumentError> {
FuncArgs::from_args(vm, args).map(Self)
}
}

#[pyfunction]
fn connect(args: FuncArgs, vm: &VirtualMachine) -> PyResult {
fn connect(ConnectForwardArgs(args): ConnectForwardArgs, vm: &VirtualMachine) -> PyResult {
if args.args.len() > 1 {
return Err(vm.new_type_error(format!(
"connect() takes at most 1 positional arguments ({} given)",
Expand Down Expand Up @@ -1043,13 +1056,18 @@ mod _sqlite3 {
alt(obj)
}

#[pyfunction]
fn adapt(
#[derive(FromArgs)]
struct AdaptArgs {
#[pyarg(positional)]
obj: PyObjectRef,
#[pyarg(positional, default, py_default = "PrepareProtocolType")]
proto: OptionalArg<Option<PyTypeRef>>,
#[pyarg(positional, optional)]
alt: OptionalArg<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult {
}

#[pyfunction]
fn adapt(AdaptArgs { obj, proto, alt }: AdaptArgs, vm: &VirtualMachine) -> PyResult {
if matches!(proto, OptionalArg::Present(None)) {
return if let OptionalArg::Present(alt) = alt {
Ok(alt)
Expand Down
35 changes: 32 additions & 3 deletions crates/stdlib/src/faulthandler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ mod decl {
cancel: bool,
fd: i32,
timeout_us: u64,
max_threads: isize,
repeat: bool,
exit: bool,
header: String,
Expand Down Expand Up @@ -683,6 +684,7 @@ mod decl {
let repeat = guard.repeat;
let exit = guard.exit;
let fd = guard.fd;
let max_threads = guard.max_threads;
let header = guard.header.clone();
#[cfg(feature = "threading")]
let thread_frame_slots = guard.thread_frame_slots.clone();
Expand All @@ -691,10 +693,22 @@ mod decl {
// Timeout occurred, dump traceback
cfg_select! {
target_arch = "wasm32" => {
let _ = (exit, fd, &header);
let _ = (exit, fd, max_threads, &header);
}
_ => {
puts_bytes(fd, header.as_bytes());
let mut nthreads = 0;
let mut begin_thread_dump = || {
if nthreads != 0 {
puts(fd, "\n");
}
if nthreads >= max_threads {
puts(fd, "...\n");
return false;
}
nthreads += 1;
true
};

// Use thread frame slots when threading is enabled (includes all threads).
// Fall back to live frame walking for non-threaded builds.
Expand All @@ -710,6 +724,9 @@ mod decl {
// frames are still alive because the target thread
// is executing (inside a blocking call).
for (tid, slot) in &thread_frame_slots {
if !begin_thread_dump() {
break;
}
let iframe_ptr =
slot.top_iframe.load(core::sync::atomic::Ordering::Relaxed)
as *const rustpython_vm::frame::InterpreterFrame;
Expand All @@ -733,6 +750,9 @@ mod decl {
}
all(not(unix), feature = "threading") => {
for (tid, slot) in &thread_frame_slots {
if !begin_thread_dump() {
break;
}
let iframe_ptr =
slot.top_iframe.load(core::sync::atomic::Ordering::Relaxed)
as *const rustpython_vm::frame::InterpreterFrame;
Expand All @@ -755,8 +775,10 @@ mod decl {
}
}
_ => {
write_thread_id(fd, current_thread_id(), false);
dump_live_frames(fd);
if begin_thread_dump() {
write_thread_id(fd, current_thread_id(), false);
dump_live_frames(fd);
}
}
}

Expand All @@ -783,6 +805,8 @@ mod decl {
file: OptionalArg<PyObjectRef>,
#[pyarg(any, default = ArgIntoBool::FALSE)]
exit: ArgIntoBool,
#[pyarg(named, default = 100)]
max_threads: isize,
}

#[pyfunction]
Expand Down Expand Up @@ -822,6 +846,11 @@ mod decl {
cancel: false,
fd,
timeout_us,
max_threads: if args.max_threads == 0 {
100
} else {
args.max_threads
},
repeat: args.repeat.into_bool(),
exit: args.exit.into_bool(),
header,
Expand Down
28 changes: 16 additions & 12 deletions crates/stdlib/src/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1889,9 +1889,15 @@ mod _socket {

#[cfg(all(unix, not(target_os = "redox")))]
#[derive(FromArgs)]
struct SendmsgAddr {
struct SendmsgArgs {
#[pyarg(positional)]
buffers: PyObjectRef,
#[pyarg(positional, default, py_default = "()")]
ancdata: OptionalArg<PyObjectRef>,
#[pyarg(positional, default = 0)]
flags: i32,
#[pyarg(positional, optional)]
addr: Option<PyObjectRef>,
address: Option<PyObjectRef>,
}

struct ReceiveIntoArgs {
Expand Down Expand Up @@ -2394,19 +2400,17 @@ mod _socket {

#[cfg(all(unix, not(target_os = "redox")))]
#[pymethod]
fn sendmsg(
zelf: &Py<Self>,
buffers: PyObjectRef,
ancdata: OptionalArg<PyObjectRef>,
flags: OptionalArg<i32>,
addr: SendmsgAddr,
vm: &VirtualMachine,
) -> PyResult<usize> {
let flags = flags.unwrap_or(0);
fn sendmsg(zelf: &Py<Self>, args: SendmsgArgs, vm: &VirtualMachine) -> PyResult<usize> {
let SendmsgArgs {
buffers,
ancdata,
flags,
address,
} = args;
let mut msg = host_socket::raw::MsgHdr::new();

let sockaddr;
if let Some(addr) = addr.addr {
if let Some(addr) = address {
sockaddr = zelf
.extract_address(addr, "sendmsg", vm)
.map_err(|e| e.into_pyexception(vm))?;
Expand Down
20 changes: 20 additions & 0 deletions extra_tests/snippets/builtin_signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import inspect
import os

from testutils import assert_raises

# __text_signature__ is generated from the Rust parameter list, so it must not
# describe parameters the function does not actually take, and must mark the
# ones it does take as positional-only.
Expand Down Expand Up @@ -72,6 +74,24 @@
assert str(inspect.signature([].__dir__)) == "()"
assert str(inspect.signature(float.fromhex)) == "(string, /)"

import _socket

if hasattr(_socket.socket, "sendmsg"):
# CPython 3.15.0 used unrepresentable defaults; 3.15.1 corrected them.
if sys.implementation.name == "cpython" and sys.version_info[:3] == (3, 15, 0):
assert _socket.socket.sendmsg.__text_signature__ == (
"($self, buffers, ancdata=<unrepresentable>, flags=0,\n"
" address=<unrepresentable>, /)"
)
assert_raises(ValueError, inspect.signature, _socket.socket.sendmsg)
else:
assert _socket.socket.sendmsg.__text_signature__ == (
"($self, buffers, ancdata=(), flags=0, address=None, /)"
)
assert str(inspect.signature(_socket.socket.sendmsg)) == (
"(self, buffers, ancdata=(), flags=0, address=None, /)"
)

# Functions whose arguments come from a FromArgs struct report the struct's
# parameters.
assert round.__text_signature__ == "($module, /, number, ndigits=None)"
Expand Down
61 changes: 61 additions & 0 deletions extra_tests/snippets/stdlib_faulthandler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import sys
import time

from testutils import assert_raises

# The watchdog's thread cap is keyword-only and uses Py_ssize_t conversion.
# Its output must distinguish an exact cap from a truncated thread dump.
if sys.platform not in ("wasi", "emscripten"):
import faulthandler

for cap in (None, 1.5):
assert_raises(TypeError, faulthandler.dump_traceback_later, 60, max_threads=cap)
for cap in (sys.maxsize + 1, -sys.maxsize - 2):
assert_raises(
OverflowError, faulthandler.dump_traceback_later, 60, max_threads=cap
)
assert_raises(
TypeError, faulthandler.dump_traceback_later, 60, False, None, False, 1
)

try:
import _thread
except ModuleNotFoundError as exc:
if exc.name != "_thread":
raise
else:
import tempfile
import threading

ready = threading.Event()
stop = threading.Event()

def watchdog_worker():
ready.set()
stop.wait()

watchdog_thread = threading.Thread(target=watchdog_worker)
watchdog_thread.start()
ready.wait()
try:
for cap, expected_threads, truncated in (
(-1, 0, True),
(0, 2, False),
(1, 1, True),
(2, 2, False),
(3, 2, False),
):
with tempfile.TemporaryFile() as output:
faulthandler.dump_traceback_later(
0.01, file=output, max_threads=cap
)
time.sleep(0.5)
faulthandler.cancel_dump_traceback_later()
output.seek(0)
traceback = output.read()
assert traceback.count(b"Thread 0x") == expected_threads, traceback
assert (b"\n...\n" in traceback) == truncated, traceback
finally:
faulthandler.cancel_dump_traceback_later()
stop.set()
watchdog_thread.join()
45 changes: 45 additions & 0 deletions extra_tests/snippets/stdlib_sqlite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
import sqlite3 as sqlite
import unittest

assert sqlite.adapt.__text_signature__ == (
"($module, obj, proto=PrepareProtocolType, alt=<unrepresentable>, /)"
)
# Check the shared parameter metadata separately from Clinic's line wrapping.
assert " ".join(sqlite.connect.__text_signature__.split()) == (
"($module, /, database, *, timeout=5.0, detect_types=0, "
"isolation_level='', check_same_thread=True, factory=ConnectionType, "
"cached_statements=128, uri=False, "
"autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL)"
)
assert " ".join(sqlite.Connection.backup.__text_signature__.split()) == (
"($self, /, target, *, pages=-1, progress=None, name='main', sleep=0.25)"
)


def check_connect_factory_signature():
calls = []
result = object()
database = object()
timeout = object()

def factory(*args, **kwargs):
calls.append((args, kwargs))
return result

# The displayed Connection signature must not validate, convert, or add
# defaults to a custom factory's call. CPython forwards even missing or
# duplicate database arguments, unknown keywords, and unconverted values.
cases = [
((), {}),
((database,), {"timeout": timeout, "extra": result}),
((), {"database": database}),
((database,), {"database": database}),
]
for args, kwargs in cases:
kwargs = {"factory": factory, **kwargs}
assert sqlite.connect(*args, **kwargs) is result
assert calls.pop() == (args, kwargs)
with unittest.TestCase().assertRaises(TypeError):
sqlite.connect(database, timeout, factory=factory)
assert not calls


check_connect_factory_signature()

rows = [(3,), (4,)]
cx = sqlite.connect(":memory:")
cx.execute(";")
Expand Down
Loading