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
149 changes: 79 additions & 70 deletions Lib/socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import os
import sys
from enum import IntEnum, IntFlag
from functools import partial

try:
import errno
Expand Down Expand Up @@ -122,7 +123,7 @@ def _intenum_converter(value, enum_klass):
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.",
Expand Down Expand Up @@ -180,7 +181,7 @@ def _intenum_converter(value, enum_klass):
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.",
Expand All @@ -196,7 +197,7 @@ def _intenum_converter(value, enum_klass):
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.",
Expand Down Expand Up @@ -348,75 +349,83 @@ def makefile(self, mode="r", buffering=None, *,
text.mode = mode
return text

if hasattr(os, 'sendfile'):
def _sendfile_zerocopy(self, zerocopy_func, giveup_exc_type, file,
offset=0, count=None):
"""
Send a file using a zero-copy function.
"""
import selectors

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 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)

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)
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):
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(
Expand Down
14 changes: 10 additions & 4 deletions Lib/socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,6 @@ class will essentially render the service "deaf" while one request is

# Author of the BaseServer patch: Luke Kenneth Casson Leighton

__version__ = "0.4"


import socket
import selectors
import os
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -861,3 +858,12 @@ def setup(self):

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}")
91 changes: 88 additions & 3 deletions Lib/test/test_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import _thread as thread
import array
import contextlib
import decimal
import errno
import fractions
import gc
import io
import itertools
Expand Down Expand Up @@ -1349,10 +1351,20 @@ def testDefaultTimeout(self):
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)
Comment on lines +1354 to +1355

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the timeout test body unchanged

Do not land this imported test-body change: it changes timeout test data and assertions, while the repository policy permits test-file edits only for adding or removing expected-failure decorators. Keep this test body unchanged and validate the new behavior without modifying Lib/test.

AGENTS.md reference: AGENTS.md:L278-L282

Useful? React with 👍 / 👎.

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)
Expand Down Expand Up @@ -1580,6 +1592,40 @@ def testSetSockOpt(self):
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:
Expand Down Expand Up @@ -2435,6 +2481,45 @@ def testCrucialConstants(self):
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
Expand Down
Loading
Loading