Align socket and socketserver with Python 3.15 (#9156)

Import the canonical socket libraries and tests through update_lib, preserving the existing AF_ALG expected failures. Match setsockopt index/buffer conversion and error ordering, mask optlen with the final CPython deprecation warning, reject oversized Windows buffers before the host call, and preserve arbitrary-precision getaddrinfo ports and empty canonical names.

Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-16 10:14:55 +09:00
1 parent 1ee9a29062
commit fd5b30eb5e
5 files changed
+318 -102

No files matched your search

+79 -70
View File
@@ -56,6 +56,7 @@ import io
import os
import sys
from enum import IntEnum, IntFlag
from functools import partial
try:
import errno
@@ -122,7 +123,7 @@ if sys.platform.lower().startswith("win"):
10004: "The operation was interrupted.",
10009: "A bad file handle was passed.",
10013: "Permission denied.",
10014: "A fault occurred on the network??",
10014: "An invalid pointer was passed.",
10022: "An invalid operation was attempted.",
10024: "Too many open files.",
10035: "The socket operation would block.",
@@ -180,7 +181,7 @@ if sys.platform.lower().startswith("win"):
11001: "Host not found.",
11002: "Nonauthoritative host not found.",
11003: "This is a nonrecoverable error.",
11004: "Valid name, no data record requested type.",
11004: "Valid name, no data record of requested type.",
11005: "QoS receivers.",
11006: "QoS senders.",
11007: "No QoS senders.",
@@ -196,7 +197,7 @@ if sys.platform.lower().startswith("win"):
11017: "QoS flowspec error.",
11018: "Invalid QoS provider buffer.",
11019: "Invalid QoS filter style.",
11020: "Invalid QoS filter style.",
11020: "Invalid QoS filter type.",
11021: "Incorrect QoS filter count.",
11022: "Invalid QoS object length.",
11023: "Incorrect QoS flow count.",
@@ -348,75 +349,83 @@ class socket(_socket.socket):
text.mode = mode
return text
def _sendfile_zerocopy(self, zerocopy_func, giveup_exc_type, file,
offset=0, count=None):
"""
Send a file using a zero-copy function.
"""
import selectors
self._check_sendfile_params(file, offset, count)
sockno = self.fileno()
try:
fileno = file.fileno()
except (AttributeError, io.UnsupportedOperation) as err:
raise giveup_exc_type(err) # not a regular file
try:
fsize = os.fstat(fileno).st_size
except OSError as err:
raise giveup_exc_type(err) # not a regular file
if not fsize:
return 0 # empty file
# Truncate to 1GiB to avoid OverflowError, see bpo-38319.
blocksize = min(count or fsize, 2 ** 30)
timeout = self.gettimeout()
if timeout == 0:
raise ValueError("non-blocking sockets are not supported")
# poll/select have the advantage of not requiring any
# extra file descriptor, contrarily to epoll/kqueue
# (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
selector = selectors.PollSelector()
else:
selector = selectors.SelectSelector()
selector.register(sockno, selectors.EVENT_WRITE)
total_sent = 0
# localize variable access to minimize overhead
selector_select = selector.select
try:
while True:
if timeout and not selector_select(timeout):
raise TimeoutError('timed out')
if count:
blocksize = min(count - total_sent, blocksize)
if blocksize <= 0:
break
try:
sent = zerocopy_func(fileno, offset, blocksize)
except BlockingIOError:
if not timeout:
# Block until the socket is ready to send some
# data; avoids hogging CPU resources.
selector_select()
continue
except OSError as err:
if total_sent == 0:
# We can get here for different reasons, the main
# one being 'file' is not a regular mmap(2)-like
# file, in which case we'll fall back on using
# plain send().
raise giveup_exc_type(err)
raise err from None
else:
if sent == 0:
break # EOF
offset += sent
total_sent += sent
return total_sent
finally:
if total_sent > 0 and hasattr(file, 'seek'):
file.seek(offset)
if hasattr(os, 'sendfile'):
def _sendfile_use_sendfile(self, file, offset=0, count=None):
# Lazy import to improve module import time
import selectors
self._check_sendfile_params(file, offset, count)
sockno = self.fileno()
try:
fileno = file.fileno()
except (AttributeError, io.UnsupportedOperation) as err:
raise _GiveupOnSendfile(err) # not a regular file
try:
fsize = os.fstat(fileno).st_size
except OSError as err:
raise _GiveupOnSendfile(err) # not a regular file
if not fsize:
return 0 # empty file
# Truncate to 1GiB to avoid OverflowError, see bpo-38319.
blocksize = min(count or fsize, 2 ** 30)
timeout = self.gettimeout()
if timeout == 0:
raise ValueError("non-blocking sockets are not supported")
# poll/select have the advantage of not requiring any
# extra file descriptor, contrarily to epoll/kqueue
# (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
selector = selectors.PollSelector()
else:
selector = selectors.SelectSelector()
selector.register(sockno, selectors.EVENT_WRITE)
total_sent = 0
# localize variable access to minimize overhead
selector_select = selector.select
os_sendfile = os.sendfile
try:
while True:
if timeout and not selector_select(timeout):
raise TimeoutError('timed out')
if count:
blocksize = min(count - total_sent, blocksize)
if blocksize <= 0:
break
try:
sent = os_sendfile(sockno, fileno, offset, blocksize)
except BlockingIOError:
if not timeout:
# Block until the socket is ready to send some
# data; avoids hogging CPU resources.
selector_select()
continue
except OSError as err:
if total_sent == 0:
# We can get here for different reasons, the main
# one being 'file' is not a regular mmap(2)-like
# file, in which case we'll fall back on using
# plain send().
raise _GiveupOnSendfile(err)
raise err from None
else:
if sent == 0:
break # EOF
offset += sent
total_sent += sent
return total_sent
finally:
if total_sent > 0 and hasattr(file, 'seek'):
file.seek(offset)
return self._sendfile_zerocopy(
partial(os.sendfile, self.fileno()),
_GiveupOnSendfile,
file, offset, count,
)
else:
def _sendfile_use_sendfile(self, file, offset=0, count=None):
raise _GiveupOnSendfile(
+10 -4
View File
@@ -120,9 +120,6 @@ BaseServer:
# Author of the BaseServer patch: Luke Kenneth Casson Leighton
__version__ = "0.4"
import socket
import selectors
import os
@@ -441,7 +438,7 @@ class TCPServer(BaseServer):
socket_type = socket.SOCK_STREAM
request_queue_size = 5
request_queue_size = getattr(socket, "SOMAXCONN", 5)
allow_reuse_address = False
@@ -861,3 +858,12 @@ class DatagramRequestHandler(BaseRequestHandler):
def finish(self):
self.socket.sendto(self.wfile.getvalue(), self.client_address)
def __getattr__(name):
if name == "__version__":
from warnings import _deprecated
_deprecated("__version__", remove=(3, 20))
return "0.4" # Do not change
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+88 -3
View File
@@ -8,7 +8,9 @@ from test.support.import_helper import ensure_lazy_imports
import _thread as thread
import array
import contextlib
import decimal
import errno
import fractions
import gc
import io
import itertools
@@ -1349,10 +1351,20 @@ class GeneralModuleTests(unittest.TestCase):
self.assertEqual(s.gettimeout(), None)
# Set the default timeout to 10, and see if it propagates
with socket_setdefaulttimeout(10):
self.assertEqual(socket.getdefaulttimeout(), 10)
with socket_setdefaulttimeout(10.125):
self.assertEqual(socket.getdefaulttimeout(), 10.125)
with socket.socket() as sock:
self.assertEqual(sock.gettimeout(), 10)
self.assertEqual(sock.gettimeout(), 10.125)
socket.setdefaulttimeout(decimal.Decimal('11.125'))
self.assertEqual(socket.getdefaulttimeout(), 11.125)
with socket.socket() as sock:
self.assertEqual(sock.gettimeout(), 11.125)
socket.setdefaulttimeout(fractions.Fraction(97, 8))
self.assertEqual(socket.getdefaulttimeout(), 12.125)
with socket.socket() as sock:
self.assertEqual(sock.gettimeout(), 12.125)
# Reset the default timeout to None, and see if it propagates
socket.setdefaulttimeout(None)
@@ -1580,6 +1592,40 @@ class GeneralModuleTests(unittest.TestCase):
reuse = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
self.assertFalse(reuse == 0, "failed to set reuse mode")
def test_setsockopt_errors(self):
# See issue #107546.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.addCleanup(sock.close)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # No error expected.
with self.assertRaises(OverflowError):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 2 ** 100)
with self.assertRaises(OverflowError):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, - 2 ** 100)
with self.assertRaises(OverflowError):
sock.setsockopt(socket.SOL_SOCKET, 2 ** 100, 1)
with self.assertRaises(OverflowError):
sock.setsockopt(2 ** 100, socket.SO_REUSEADDR, 1)
with self.assertRaisesRegex(TypeError, "socket option should be int, bytes-like object or None"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, dict())
with self.assertRaisesRegex(TypeError, "requires 4 arguments when the third argument is None"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, None)
with self.assertRaisesRegex(TypeError, "only takes 4 arguments when the third argument is None"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1, 2)
with self.assertRaisesRegex(TypeError, "takes at least 3 arguments"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
with self.assertRaisesRegex(TypeError, "takes at most 4 arguments"):
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1, 2, 3)
def testSendAfterClose(self):
# testing send() after close() with timeout
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@@ -2435,6 +2481,45 @@ class ISOTPTest(unittest.TestCase):
socket.CAN_ISOTP
socket.SOCK_DGRAM
@unittest.skipUnless(hasattr(socket, "SOL_CAN_ISOTP"),
"missing <linux/can/isotp.h>")
def testISOTP(self):
socket.SOL_CAN_ISOTP
socket.CAN_ISOTP_OPTS
socket.CAN_ISOTP_RECV_FC
socket.CAN_ISOTP_TX_STMIN
socket.CAN_ISOTP_RX_STMIN
socket.CAN_ISOTP_LL_OPTS
socket.CAN_ISOTP_LISTEN_MODE
socket.CAN_ISOTP_EXTEND_ADDR
socket.CAN_ISOTP_TX_PADDING
socket.CAN_ISOTP_RX_PADDING
socket.CAN_ISOTP_CHK_PAD_LEN
socket.CAN_ISOTP_CHK_PAD_DATA
socket.CAN_ISOTP_HALF_DUPLEX
socket.CAN_ISOTP_FORCE_TXSTMIN
socket.CAN_ISOTP_FORCE_RXSTMIN
socket.CAN_ISOTP_RX_EXT_ADDR
socket.CAN_ISOTP_WAIT_TX_DONE
# This constant is not always available
# socket.CAN_ISOTP_SF_BROADCAST
socket.CAN_ISOTP_DEFAULT_FLAGS
socket.CAN_ISOTP_DEFAULT_EXT_ADDRESS
socket.CAN_ISOTP_DEFAULT_PAD_CONTENT
socket.CAN_ISOTP_DEFAULT_FRAME_TXTIME
socket.CAN_ISOTP_DEFAULT_RECV_BS
socket.CAN_ISOTP_DEFAULT_EXT_ADDRESS
socket.CAN_ISOTP_DEFAULT_RECV_STMIN
socket.CAN_ISOTP_DEFAULT_RECV_WFTMAX
socket.CAN_ISOTP_DEFAULT_LL_MTU
socket.CAN_ISOTP_DEFAULT_LL_TX_DL
socket.CAN_ISOTP_DEFAULT_LL_TX_FLAGS
def testCreateSocket(self):
with socket.socket(socket.PF_CAN, socket.SOCK_RAW, socket.CAN_RAW) as s:
pass
+18
View File
@@ -17,6 +17,7 @@ from test.support import reap_children, verbose
from test.support import os_helper
from test.support import socket_helper
from test.support import threading_helper
from test.support import warnings_helper
test.support.requires("network")
@@ -43,6 +44,7 @@ def receive(sock, n, timeout=test.support.SHORT_TIMEOUT):
raise RuntimeError("timed out on %r" % (sock,))
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@test.support.requires_fork()
@contextlib.contextmanager
def simple_subprocess(testcase):
@@ -173,6 +175,7 @@ class SocketServerTest(unittest.TestCase):
socketserver.StreamRequestHandler,
self.stream_examine)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_forking
def test_ForkingTCPServer(self):
with simple_subprocess(self):
@@ -192,6 +195,7 @@ class SocketServerTest(unittest.TestCase):
socketserver.StreamRequestHandler,
self.stream_examine)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_unix_sockets
@requires_forking
def test_ForkingUnixStreamServer(self):
@@ -210,6 +214,7 @@ class SocketServerTest(unittest.TestCase):
socketserver.DatagramRequestHandler,
self.dgram_examine)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_forking
def test_ForkingUDPServer(self):
with simple_subprocess(self):
@@ -233,6 +238,7 @@ class SocketServerTest(unittest.TestCase):
socketserver.DatagramRequestHandler,
self.dgram_examine)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_unix_sockets
@requires_forking
def test_ForkingUnixDatagramServer(self):
@@ -318,11 +324,13 @@ class ErrorHandlerTest(unittest.TestCase):
self.assertIs(cm.exc_type, SystemExit)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_forking
def test_forking_handled(self):
ForkingErrorTestServer(ValueError)
self.check_result(handled=True)
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@requires_forking
def test_forking_not_handled(self):
ForkingErrorTestServer(SystemExit)
@@ -507,5 +515,15 @@ class MiscTestCase(unittest.TestCase):
server.server_close()
class TestModule(unittest.TestCase):
def test_deprecated__version__(self):
with self.assertWarnsRegex(
DeprecationWarning,
"'__version__' is deprecated and slated for removal in Python 3.20",
) as cm:
getattr(socketserver, "__version__")
self.assertEqual(cm.filename, __file__)
if __name__ == "__main__":
unittest.main()
+123 -25
View File
@@ -21,7 +21,7 @@ mod _socket {
},
convert::{IntoPyException, ToPyObject, TryFromBorrowedObject, TryFromObject},
function::{
ArgBytesLike, ArgIntoFloat, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath,
ArgBytesLike, ArgIndex, ArgIntoFloat, ArgMemoryBuffer, ArgStrOrBytesLike, FsPath,
FuncArgs, OptionalArg,
},
types::{Constructor, DefaultConstructor, Destructor, Initializer, Representable},
@@ -2441,28 +2441,114 @@ mod _socket {
#[pymethod]
fn setsockopt(
zelf: &Py<Self>,
level: i32,
name: i32,
value: Option<Either<ArgBytesLike, i32>>,
optlen: OptionalArg<u32>,
args: FuncArgs,
vm: &VirtualMachine,
) -> Result<(), IoOrPyException> {
if !args.kwargs.is_empty() {
return Err(vm
.new_type_error("setsockopt() takes no keyword arguments")
.into());
}
let nargs = args.args.len();
if nargs < 3 {
return Err(vm
.new_type_error(format!(
"setsockopt() takes at least 3 arguments ({nargs} given)"
))
.into());
}
if nargs > 4 {
return Err(vm
.new_type_error(format!(
"setsockopt() takes at most 4 arguments ({nargs} given)"
))
.into());
}
let (level, name, value, optlen): (
i32,
i32,
Option<PyObjectRef>,
OptionalArg<ArgIndex>,
) = args.bind(vm)?;
let optlen = match optlen {
OptionalArg::Present(index) => {
let index = index.as_ref();
// CPython's unsigned-int bitfield conversion masks the value,
// warning only when it fits neither a signed nor unsigned int.
if !index.as_bigint().to_i64().is_some_and(|value| {
(i64::from(i32::MIN)..=i64::from(u32::MAX)).contains(&value)
}) {
crate::vm::warn::warn(
vm.ctx.new_str("integer value out of range").into(),
Some(vm.ctx.exceptions.deprecation_warning.to_owned()),
1,
None,
vm,
)?;
}
OptionalArg::Present(index.as_u32_mask())
}
OptionalArg::Missing => OptionalArg::Missing,
};
enum Value {
Int(i32),
Buffer(ArgBytesLike),
Null(u32),
}
let value = match (value, optlen) {
(Some(value), OptionalArg::Missing) => {
if let Some(index) = value.try_index_opt(vm) {
Value::Int(index?.try_to_primitive::<i32>(vm)?)
} else if value.check_buffer() {
let buffer = ArgBytesLike::try_from_object(vm, value)?;
#[cfg(windows)]
if buffer.len() > i32::MAX as usize {
return Err(vm
.new_overflow_error(format!(
"socket option is larger than {} bytes",
i32::MAX
))
.into());
}
Value::Buffer(buffer)
} else {
return Err(vm
.new_type_error(format!(
"socket option should be int, bytes-like object or None (got {})",
value.class().fully_qualified_name(vm)?
))
.into());
}
}
(None, OptionalArg::Present(optlen)) => Value::Null(optlen),
(None, OptionalArg::Missing) => {
return Err(vm
.new_type_error(
"setsockopt() requires 4 arguments when the third argument is None",
)
.into());
}
(Some(value), OptionalArg::Present(_)) => {
return Err(vm
.new_type_error(format!(
"setsockopt() only takes 4 arguments when the third argument is None (got {})",
value.class().fully_qualified_name(vm)?
))
.into());
}
};
// Convert Python values before locking the socket; callbacks may close it.
let sock = zelf.sock()?;
let fd = sock_fileno(&sock);
match (value, optlen) {
(Some(Either::A(b)), OptionalArg::Missing) => {
b.with_ref(|b| host_socket::setsockopt_bytes(fd as _, level, name, b))?
match &value {
Value::Int(value) => {
host_socket::setsockopt_int(fd as _, level, name, *value)?;
}
(Some(Either::B(val)), OptionalArg::Missing) => {
host_socket::setsockopt_int(fd as _, level, name, val)?
Value::Buffer(buffer) => {
buffer.with_ref(|b| host_socket::setsockopt_bytes(fd as _, level, name, b))?;
}
(None, OptionalArg::Present(optlen)) => {
host_socket::setsockopt_none(fd as _, level, name, optlen)?
}
_ => {
return Err(vm
.new_type_error("expected the value arg xor the optlen arg")
.into());
Value::Null(optlen) => {
host_socket::setsockopt_none(fd as _, level, name, *optlen)?;
}
}
Ok(())
@@ -2962,7 +3048,7 @@ mod _socket {
#[pyarg(any)]
host: Option<ArgStrOrBytesLike>,
#[pyarg(any)]
port: Option<Either<ArgStrOrBytesLike, i32>>,
port: Option<PyObjectRef>,
#[pyarg(any, default = c::AF_UNSPEC)]
family: i32,
@@ -3014,10 +3100,23 @@ mod _socket {
};
let host = host_encoded.as_deref();
// Encode port: str/bytes as service name, int as port number
let port_encoded: Option<String> = match opts.port.as_ref() {
Some(Either::A(sb)) => {
let port_str = match sb {
// Check the index protocol before string/bytes conversion, including for
// subclasses, and preserve any exception raised by __index__.
let port_index = opts
.port
.as_ref()
.and_then(|port| port.try_index_opt(vm))
.transpose()?;
let port_encoded: Option<String> = match (port_index, opts.port.as_ref()) {
(Some(index), _) => Some(crate::vm::builtins::PyInt::repr_str(&index, vm)?),
(None, Some(port)) => {
if !port.fast_isinstance(vm.ctx.types.str_type)
&& !port.fast_isinstance(vm.ctx.types.bytes_type)
{
return Err(vm.new_os_error("Int or String expected").into());
}
let sb = ArgStrOrBytesLike::try_from_object(vm, port.clone())?;
let port_str = match &sb {
ArgStrOrBytesLike::Str(s) => {
// For str, check for surrogates and raise UnicodeEncodeError if found
s.to_str()
@@ -3055,8 +3154,7 @@ mod _socket {
};
Some(port_str)
}
Some(Either::B(i)) => Some(i.to_string()),
None => None,
(None, None) => None,
};
let port = port_encoded.as_deref();
@@ -3070,7 +3168,7 @@ mod _socket {
ai.address,
ai.socktype,
ai.protocol,
ai.canonname,
ai.canonname.unwrap_or_default(),
get_ip_addr_tuple(&ai.sockaddr, vm),
))
.into()