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
2 changes: 2 additions & 0 deletions Doc/library/curses.panel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Panel objects
.. method:: panel.replace(win)

Change the window associated with the panel to the window *win*.
Raise :exc:`curses.panel.error` if *win* has been detached from its
screen by :meth:`screen.close() <curses.screen.close>`.


.. method:: panel.set_userptr(obj)
Expand Down
14 changes: 12 additions & 2 deletions Lib/_pyio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,9 +1003,19 @@ def tell(self):
def peek(self, size=0):
if self.closed:
raise ValueError("peek on closed file")
try:
size_index = size.__index__
except AttributeError:
raise TypeError(f"{size!r} is not an integer")
else:
size = size_index()

if size < 1:
return self._buffer[self._pos:self._pos + io.DEFAULT_BUFFER_SIZE]
return self._buffer[self._pos:self._pos + size]
size = io.DEFAULT_BUFFER_SIZE

with self._lock:
b = self._buffer[self._pos:self._pos + size]
return b.take_bytes()

def truncate(self, pos=None):
if self.closed:
Expand Down
5 changes: 5 additions & 0 deletions Lib/asyncio/selector_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1198,8 +1198,13 @@ def writelines(self, list_of_data):
return

for data in list_of_data:
# gh-155888: an empty chunk can never be drained, so never buffer it
if not data:
continue
self._buffer.append(memoryview(data))
self._buffer_size += len(data)
if not self._buffer:
return
self._write_ready()
# If the entire buffer couldn't be written, register a write handler
if self._buffer:
Expand Down
8 changes: 7 additions & 1 deletion Lib/multiprocessing/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from . import process
from . import reduction
from . import util

__all__ = ()

Expand Down Expand Up @@ -333,7 +334,12 @@ def _check_available(self):
# bpo-33725: running arbitrary code after fork() is no longer reliable
# on macOS since macOS 10.14 (Mojave). Use spawn by default instead.
# gh-84559: We changed everyones default to a thread safeish one in 3.14.
if reduction.HAVE_SEND_HANDLE and sys.platform != 'darwin':
if (
reduction.HAVE_SEND_HANDLE
and sys.platform != 'darwin'
# gh-155717: forkserver requires to write temporary files
and util._has_writeable_tempdir()
):
_default_context = DefaultContext(_concrete_contexts['forkserver'])
else:
_default_context = DefaultContext(_concrete_contexts['spawn'])
Expand Down
31 changes: 28 additions & 3 deletions Lib/multiprocessing/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import os
import itertools
import sys
import tempfile
import weakref
import atexit
import threading # we want threading to install it's
Expand Down Expand Up @@ -143,6 +144,7 @@ def is_abstract_socket_namespace(address):
# On Windows platforms, we do not create AF_UNIX sockets.
_SUN_PATH_MAX = None if os.name == 'nt' else 92


def _remove_temp_dir(rmtree, tempdir):
rmtree(tempdir)

Expand All @@ -152,7 +154,8 @@ def _remove_temp_dir(rmtree, tempdir):
if current_process is not None:
current_process._config['tempdir'] = None

def _get_base_temp_dir(tempfile):

def _get_base_temp_dir():
"""Get a temporary directory where socket files will be created.

To prevent additional imports, pass a pre-imported 'tempfile' module.
Expand Down Expand Up @@ -208,12 +211,13 @@ def _get_base_temp_dir(tempfile):
assert len(base_system_tempdir) + 14 + 14 < _SUN_PATH_MAX
return base_system_tempdir


def get_temp_dir():
# get name of a temp directory which will be automatically cleaned up
tempdir = process.current_process()._config.get('tempdir')
if tempdir is None:
import shutil, tempfile
base_tempdir = _get_base_temp_dir(tempfile)
import shutil
base_tempdir = _get_base_temp_dir()
tempdir = tempfile.mkdtemp(prefix='pymp-', dir=base_tempdir)
info('created temp directory %s', tempdir)
# keep a strong reference to shutil.rmtree(), since the finalizer
Expand All @@ -223,6 +227,27 @@ def get_temp_dir():
process.current_process()._config['tempdir'] = tempdir
return tempdir


def _has_writeable_tempdir():
# 'forkserver' requires writeable temporary files. This function is
# called to determine the default context's start method.
#
# See: https://github.com/python/cpython/issues/155717.

path = _get_base_temp_dir()
if path is None:
return False

# os.access() is advisory and racy. It can lie on read-only filesystems,
# NFS/network mounts, containers, and immutable-flag files, so we simply
# try to create a file to check if this works and delete it otherwise.
try:
with tempfile.NamedTemporaryFile(dir=path):
return True
except OSError:
return False


#
# Support for reinitialization of objects when bootstrapping a child process
#
Expand Down
6 changes: 1 addition & 5 deletions Lib/pickle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,11 +1623,7 @@ def load_dict(self):
def _instantiate(self, klass, args):
if (args or not isinstance(klass, type) or
hasattr(klass, "__getinitargs__")):
try:
value = klass(*args)
except TypeError as err:
raise TypeError("in constructor for %s: %s" %
(klass.__name__, str(err)), err.__traceback__)
value = klass(*args)
else:
value = klass.__new__(klass)
self.append(value)
Expand Down
36 changes: 36 additions & 0 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import struct
import tempfile
import operator
import pathlib
import pickle
import weakref
import warnings
Expand Down Expand Up @@ -6355,6 +6356,41 @@ def test_nested_startmethod(self):
# there is no synchronization in the test.
self.assertSetEqual(set(results), set([2, 1]))

@unittest.skipIf(os.name == "nt", "requires POSIX")
@support.requires_non_root_user
@support.subTests("mode", [
os.R_OK, # read-only directory
os.R_OK | os.X_OK, # read-only directory
os.W_OK # write-only directory _without_ permissions for creating files
])
def test_forkserver_requires_writeable_tempdir(self, mode):
# Regression test to ensure that the defualt start method is
# not 'forkserver' when the temporary directory is not writeable.
#
# See https://github.com/python/cpython/issues/155717.

cmd = '''if 1:
import os, tempfile
# We fake the read-onlyiness of /tmp (which is a fallback when
# the user-defined TMPDIR is not acceptable) by hardcoding the
# temporary directory for this specific test.
tempfile.tempdir = os.environ["TMPDIR"]

# Imported after patching 'tempfile' so that the default start
# method is deduced according to the permissions of TMPDIR.
import multiprocessing
if __name__ == "__main__":
print(multiprocessing.get_start_method())
'''

with support.os_helper.temp_dir() as root:
TMPDIR = pathlib.Path(root, "TMPDIR")
TMPDIR.mkdir(mode=mode)
file = pathlib.Path(TMPDIR, "file")
self.assertRaises(OSError, file.touch)
_, out, err = script_helper.assert_python_ok('-c', cmd, TMPDIR=TMPDIR)
self.assertEqual(out.decode().strip(), "spawn")


@unittest.skipIf(sys.platform == "win32",
"test semantics don't make sense on Windows")
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/picklecommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ class E(C):
def __getinitargs__(self):
return ()

# For test_load_bad_constructor
class BadConstructor:
def __init__(self, *args):
raise TypeError("bad constructor")

import __main__
__main__.C = C
C.__module__ = "__main__"
__main__.D = D
D.__module__ = "__main__"
__main__.E = E
E.__module__ = "__main__"
__main__.BadConstructor = BadConstructor
BadConstructor.__module__ = "__main__"

# Simple mutable object.
class Object(object):
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,20 @@ def test_load_classic_instance(self):
b'q\x00oq\x01}q\x02b.').replace(b'X', xname)
self.assert_is_copy(X(*args), self.loads(pickle2))

def test_load_bad_constructor(self):
# gh-154002: a TypeError raised by an old-style instance constructor
# during INST/OBJ unpickling propagates unchanged. The pure-Python
# unpickler used to replace it with one that carried the traceback
# object in its args.
# 0: ( MARK
# 1: I INT 1
# 4: i INST '__main__ BadConstructor' (MARK at 0)
# 28: . STOP
data = b'(I1\ni__main__\nBadConstructor\n.'
with self.assertRaises(TypeError) as cm:
self.loads(data)
self.assertEqual(cm.exception.args, ("bad constructor",))

def test_maxint64(self):
maxint64 = (1 << 63) - 1
data = b'I' + str(maxint64).encode("ascii") + b'\n.'
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,19 @@ def writer(data):
r.close()
self.assertEqual(read, data)

def test_writelines_empty_chunk(self):
# gh-155888: an empty chunk can never be drained, so never buffer it
rsock, wsock = socket.socketpair()
self.addCleanup(rsock.close)

async def main():
reader, writer = await asyncio.open_connection(sock=wsock)
writer.writelines([b'data', b''])
writer.close()
await asyncio.wait_for(writer.wait_closed(), support.SHORT_TIMEOUT)

self.loop.run_until_complete(main())

@unittest.skipUnless(hasattr(signal, 'SIGKILL'), 'No SIGKILL')
def test_add_signal_handler(self):
caught = 0
Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_curses.py
Original file line number Diff line number Diff line change
Expand Up @@ -3056,6 +3056,33 @@ def test_window_keeps_screen_alive(self):
win.addstr(0, 0, 'still alive')
win.refresh()

@unittest.skipUnless(hasattr(curses.screen, 'use'),
'requires curses.screen.use()')
def test_window_made_in_use_keeps_its_screen_alive(self):
# use() makes its screen current for the callback, so a window created
# there belongs to that screen and must keep it alive, not the screen
# that was current before.
s = self.make_pty()
s2 = self.make_pty()
a = curses.newterm('xterm', s, s)
b = curses.newterm('xterm', s2, s2) # current screen is b
win = a.use(lambda scr: curses.newwin(3, 3))
del a
gc_collect()
win.addstr(0, 0, 'x')
b.stdscr.refresh()

@unittest.skipUnless(hasattr(curses.screen, 'use'),
'requires curses.screen.use()')
def test_initscr_in_use_returns_its_screen(self):
# initscr() returns the standard window of the current screen, and
# inside use() that is the used screen.
s = self.make_pty()
s2 = self.make_pty()
a = curses.newterm('xterm', s, s)
b = curses.newterm('xterm', s2, s2) # current screen is b
self.assertIs(a.use(lambda scr: curses.initscr()), a.stdscr)

def test_screen_freed(self):
# Dropping all references to a (non-current) screen and its windows
# frees it without error.
Expand All @@ -3079,6 +3106,22 @@ def test_close(self):
# close() is idempotent.
screen.close()

@requires_curses_func('panel')
def test_close_then_panel_replace(self):
# A detached window has no underlying curses window, so replace()
# must reject it. It used to be accepted, and the panel then
# crashed inside curses on its next use.
s = self.make_pty()
screen = curses.newterm('xterm', s, s)
win = screen.stdscr
panel = curses.panel.new_panel(curses.newwin(3, 6, 0, 0))
# Drop the panel from the global stack before later tests inspect it.
self.addCleanup(gc_collect)
screen.close()
self.assertRaises(curses.panel.error, panel.replace, win)
# The panel kept its own window, so it still works.
panel.move(1, 1)

@unittest.skipUnless(hasattr(curses, 'new_prescr'),
'requires curses.new_prescr()')
def test_new_prescr(self):
Expand Down
5 changes: 5 additions & 0 deletions Lib/test/test_io/test_memoryio.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,11 @@ def test_peek(self):
buf = self.buftype("1234567890")
with self.ioclass(buf) as memio:
self.assertEqual(memio.tell(), 0)
# bytearray(b'1') == b'1', so the type has to be asserted separately.
self.assertIsInstance(memio.peek(), bytes)
self.assertIsInstance(memio.peek(1), bytes)
self.assertEqual(memio.peek(IntLike(3)), buf[:3])
self.assertRaises(TypeError, memio.peek, 1.5)
self.assertEqual(memio.peek(1), buf[:1])
self.assertEqual(memio.peek(1), buf[:1])
self.assertEqual(memio.peek(), buf)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The pure-Python :mod:`pickle` unpickler no longer replaces a :exc:`TypeError`
raised by an old-style instance constructor with a new one carrying the
traceback object in its ``args``. The original error now propagates, as it
already did in the C implementation.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`multiprocessing`'s default start method on systems with non-writeable
tempfile filesystem is now :ref:`"spawn" <multiprocessing-start-methods>`
instead of ``"forkserver"``. Patch by Bénédikt Tran.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :meth:`asyncio.WriteTransport.writelines` hanging the transport when the
last data chunk is empty.
6 changes: 6 additions & 0 deletions Modules/_curses_panel.c
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ _curses_panel_panel_replace_impl(PyCursesPanelObject *self,
return NULL;
}

if (win->win == NULL) {
_curses_panel_state *state = get_curses_panel_state_by_panel(self);
PyErr_SetString(state->error, "the window has been detached");
return NULL;
}

int rtn = replace_panel(self->pan, win->win);
if (rtn == ERR) {
curses_panel_panel_set_error(self, "replace_panel", "replace");
Expand Down
5 changes: 4 additions & 1 deletion Modules/_cursesmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -5303,8 +5303,8 @@ static PyObject *
PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyCursesScreenObject *so = _PyCursesScreenObject_CAST(self);
cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
if (so->screen == NULL) {
cursesmodule_state *state = get_cursesmodule_state_by_cls(Py_TYPE(self));
PyErr_SetString(state->error, "the screen has been deleted");
return NULL;
}
Expand All @@ -5313,7 +5313,10 @@ PyCursesScreen_use(PyObject *self, PyObject *args, PyObject *kwargs)
return NULL;
}
curses_use_data data = {self, func, extra, kwargs, NULL};
PyObject *prev = state->topscreen;
state->topscreen = Py_NewRef(self);
use_screen(so->screen, curses_use_screen_cb, &data);
Py_SETREF(state->topscreen, prev);
Py_DECREF(extra);
return data.result;
}
Expand Down
Loading