diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 3775d5ac81a2736..ecf62fb6391b1b1 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -215,6 +215,8 @@ The following exceptions are the exceptions that are usually raised. The object that was accessed for the named attribute. + When possible, :attr:`name` and :attr:`obj` are set automatically. + .. versionchanged:: 3.10 Added the :attr:`name` and :attr:`obj` attributes. diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index 5152c7561fa1f26..7230224b4fc5323 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -631,7 +631,8 @@ supports sending logging messages to a remote or local Unix syslog. the form of a ``(host, port)`` tuple. If *address* is not specified, ``('localhost', 514)`` is used. The address is used to open a socket. An alternative to providing a ``(host, port)`` tuple is providing an address as a - string, for example '/dev/log'. In this case, a Unix domain socket is used to + string or a :class:`bytes` object, for example '/dev/log'. + In this case, a Unix domain socket is used to send the message to the syslog. If *facility* is not specified, :const:`LOG_USER` is used. The type of socket opened depends on the *socktype* argument, which defaults to :const:`socket.SOCK_DGRAM` and thus @@ -664,6 +665,9 @@ supports sending logging messages to a remote or local Unix syslog. .. versionchanged:: 3.14 *timeout* was added. + .. versionchanged:: next + *address* can now be a :class:`bytes` object. + .. method:: close() Closes the socket to the remote host. diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 9df5eb78d286a52..0b6a1b488fb64f9 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4876,8 +4876,9 @@ copying. Cast a memoryview to a new format or shape. *shape* defaults to ``[byte_length//new_itemsize]``, which means that the result view will be one-dimensional. The return value is a new memoryview, but - the buffer itself is not copied. Supported casts are 1D -> C-:term:`contiguous` - and C-contiguous -> 1D. + the buffer itself is not copied. Supported casts are + 1D -> C-:term:`contiguous`, C-contiguous -> 1D, and + F-contiguous -> 1D. The destination format is restricted to a single element native format in :mod:`struct` syntax. One of the formats must be a byte format @@ -4964,6 +4965,10 @@ copying. .. versionchanged:: 3.5 The source format is no longer restricted when casting to a byte view. + .. versionchanged:: next + Casting a multi-dimensional F-contiguous view to a one-dimensional + view is now supported. + .. method:: count(value, /) Count the number of occurrences of *value*. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 662defa709a246c..06e831ea1e34631 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -75,6 +75,10 @@ New features Other language changes ====================== +* :meth:`memoryview.cast` now allows casting a multidimensional + F-contiguous view to a one-dimensional view. + (Contributed by Jaemin Park in :gh:`91484`.) + * :ref:`Frame objects ` now support :mod:`weak references `. This allows associating extra data with active frames, for example in debuggers, without keeping the frames (and everything diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index 708fdd595971e85..cbb052630d71ce9 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -212,11 +212,14 @@ def interrupt(self) -> None: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) - repl_locals = {'asyncio': asyncio} - for key in {'__name__', '__package__', - '__loader__', '__spec__', - '__builtins__', '__file__'}: - repl_locals[key] = locals()[key] + repl_locals = { + 'asyncio': asyncio, + '__name__': __name__, + '__package__': None, + '__loader__': __loader__, + '__spec__': None, + '__builtins__': __builtins__, + } console = AsyncIOInteractiveConsole(repl_locals, loop) diff --git a/Lib/asyncio/staggered.py b/Lib/asyncio/staggered.py index 845aed4c6a3b352..b9e7b9eb29594fb 100644 --- a/Lib/asyncio/staggered.py +++ b/Lib/asyncio/staggered.py @@ -90,11 +90,7 @@ def task_done(task): return unhandled_exceptions.append(exc) - async def run_one_coro(ok_to_start, previous_failed) -> None: - # in eager tasks this waits for the calling task to append this task - # to running_tasks, in regular tasks this wait is a no-op that does - # not yield a future. See gh-124309. - await ok_to_start.wait() + async def run_one_coro(previous_failed) -> None: # Wait for the previous task to finish, or for delay seconds if previous_failed is not None: with contextlib.suppress(exceptions_mod.TimeoutError): @@ -110,14 +106,13 @@ async def run_one_coro(ok_to_start, previous_failed) -> None: return # Start task that will run the next coroutine this_failed = locks.Event() - next_ok_to_start = locks.Event() - next_task = loop.create_task(run_one_coro(next_ok_to_start, this_failed)) + next_task = loop.create_task( + run_one_coro(this_failed), + eager_start=False, + ) futures.future_add_to_awaited_by(next_task, parent_task) running_tasks.add(next_task) next_task.add_done_callback(task_done) - # next_task has been appended to running_tasks so next_task is ok to - # start. - next_ok_to_start.set() # Prepare place to put this coroutine's exceptions if not won exceptions.append(None) assert len(exceptions) == this_index + 1 @@ -149,13 +144,11 @@ async def run_one_coro(ok_to_start, previous_failed) -> None: propagate_cancellation_error = None try: - ok_to_start = locks.Event() - first_task = loop.create_task(run_one_coro(ok_to_start, None)) + first_task = loop.create_task(run_one_coro(None), eager_start=False) futures.future_add_to_awaited_by(first_task, parent_task) running_tasks.add(first_task) first_task.add_done_callback(task_done) - # first_task has been appended to running_tasks so first_task is ok to start. - ok_to_start.set() + # first_task has been appended to running_tasks before the event loop starts running it. propagate_cancellation_error = None # Make sure no tasks are left running if we leave this function while running_tasks: diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 1a7d534261ed720..bb98f0014f81760 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -640,7 +640,8 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): self._conn_lost = 0 self._closing = False # Set when close() or write_eof() called. - mode = os.fstat(self._fileno).st_mode + pipe_stat = os.fstat(self._fileno) + mode = pipe_stat.st_mode is_char = stat.S_ISCHR(mode) is_fifo = stat.S_ISFIFO(mode) is_socket = stat.S_ISSOCK(mode) @@ -657,7 +658,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - if is_socket or (is_fifo and not sys.platform.startswith("aix")): + # On macOS, the trick misfires for named FIFOs (but not for pipes + # created with os.pipe(), which have st_nlink == 0): the write end + # polls as readable whenever unread data sits in the FIFO, and no + # event is delivered when the read end is closed, so it can only + # ever report a false disconnection (gh-145030). The same xnu + # behaviour applies on iOS/tvOS/watchOS (sys.platform is not + # "darwin" there). + is_named_fifo_on_apple = ( + sys.platform in {"darwin", "ios", "tvos", "watchos"} + and is_fifo and pipe_stat.st_nlink > 0) + if is_socket or (is_fifo + and not sys.platform.startswith("aix") + and not is_named_fifo_on_apple): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 35233e731eb42d1..fab91e663a0f6aa 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -32,6 +32,7 @@ import os import queue import re +import socket import struct import threading import traceback @@ -1004,6 +1005,15 @@ class ConfigSocketReceiver(ThreadingTCPServer): def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT, handler=None, ready=None, verify=None): + # The host can have no IPv4 address, for example if "localhost" + # is only aliased to ::1. Leave resolution errors to the server. + try: + infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) + except OSError: + pass + else: + if not any(info[0] == socket.AF_INET for info in infos): + self.address_family = infos[0][0] ThreadingTCPServer.__init__(self, (host, port), handler) with logging._lock: self.abort = 0 @@ -1035,9 +1045,14 @@ def __init__(self, rcvr, hdlr, port, verify): self.ready = threading.Event() def run(self): - server = self.rcvr(port=self.port, handler=self.hdlr, - ready=self.ready, - verify=self.verify) + try: + server = self.rcvr(port=self.port, handler=self.hdlr, + ready=self.ready, + verify=self.verify) + except BaseException: + # Do not leave the caller waiting for ready forever. + self.ready.set() + raise if self.port == 0: self.port = server.server_address[1] self.ready.set() diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index a5394d2dbea6494..c78a30763d9fa08 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -886,8 +886,9 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT), """ Initialize a handler. - If address is specified as a string, a UNIX socket is used. To log to a - local syslogd, "SysLogHandler(address="/dev/log")" can be used. + If address is specified as a string or bytes, a UNIX socket is used. + To log to a local syslogd, "SysLogHandler(address="/dev/log")" can be + used. If facility is not specified, LOG_USER is used. If socktype is specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific socket type will be used. For Unix sockets, you can also specify a @@ -938,7 +939,7 @@ def createSocket(self): address = self.address socktype = self.socktype - if isinstance(address, str): + if not isinstance(address, (list, tuple)): self.unixsocket = True # Syslog server may be unavailable during handler initialisation. # C's openlog() function also ignores connection errors. diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 91882f41d9428ed..3fdfffdb213efc2 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1725,6 +1725,48 @@ def reader(data): self.loop.run_until_complete(proto.done) self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', + "Don't support pipes for Windows") + @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') + def test_write_named_fifo_unread_data(self): + # gh-145030: on macOS, the write end of a named FIFO polls as + # readable while unread data sits in the FIFO, which made the + # transport misinterpret the event as the reader hanging up + # and close itself. + path = os_helper.TESTFN + os.mkfifo(path) + self.assertNotEqual(os.stat(path).st_nlink, 0) + self.addCleanup(os_helper.unlink, path) + rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + self.addCleanup(os.close, rfd) + wfd = os.open(path, os.O_WRONLY | os.O_NONBLOCK) + pipeobj = io.open(wfd, 'wb', 1024) + + proto = MyWritePipeProto(loop=self.loop) + connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) + transport, p = self.loop.run_until_complete(connect) + self.assertIs(p, proto) + self.assertEqual('CONNECTED', proto.state) + + transport.write(b'1') + # Iterate the event loop while the data stays unread in the FIFO; + # the transport must not detect a false disconnection. + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertFalse(transport.is_closing()) + self.assertEqual(b'1', os.read(rfd, 1024)) + + transport.write(b'2345') + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertEqual(b'2345', os.read(rfd, 1024)) + + transport.close() + self.loop.run_until_complete(proto.done) + self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', "Don't support pipes for Windows") def test_write_pipe_disconnect_on_close(self): diff --git a/Lib/test/test_asyncio/test_transports.py b/Lib/test/test_asyncio/test_transports.py index 5e743345028bec6..12f6e40f1595fde 100644 --- a/Lib/test/test_asyncio/test_transports.py +++ b/Lib/test/test_asyncio/test_transports.py @@ -98,6 +98,29 @@ def get_write_buffer_size(self): self.assertTrue(transport._protocol_paused) self.assertEqual(transport.get_write_buffer_limits(), (128, 256)) + def test_flowcontrol_mixin_pause_writing_exception(self): + + class MyTransport(transports._FlowControlMixin, + transports.Transport): + + def get_write_buffer_size(self): + return 2000 + + loop = mock.Mock() + transport = MyTransport(loop=loop) + protocol = mock.Mock() + protocol.pause_writing.side_effect = RuntimeError("boom") + transport._protocol = protocol + transport.set_write_buffer_limits(high=1000, low=100) + transport._maybe_pause_protocol() + protocol.pause_writing.assert_called_once() + loop.call_exception_handler.assert_called_once() + args = loop.call_exception_handler.call_args[0][0] + + self.assertIn("protocol.pause_writing() failed", args["message"]) + self.assertIsInstance(args["exception"], RuntimeError) + self.assertTrue(transport._protocol_paused) + def test_flowcontrol_mixin_compute_write_limits(self): class MyTransport(transports._FlowControlMixin, diff --git a/Lib/test/test_buffer.py b/Lib/test/test_buffer.py index 453dafe2709eb2f..579e680448e94e3 100644 --- a/Lib/test/test_buffer.py +++ b/Lib/test/test_buffer.py @@ -2877,6 +2877,32 @@ class BEPoint: self.assertEqual(m2.strides, (1,)) self.assertEqual(m2.suboffsets, ()) + def test_memoryview_cast_f_contiguous_ND_1D(self): + nd = ndarray(list(range(12)), shape=[3, 4], format='B', flags=ND_FORTRAN) + m = memoryview(nd) + self.assertTrue(m.f_contiguous) + self.assertTrue(m.contiguous) + + m1 = m.cast('B') + self.assertEqual(m1.ndim, 1) + self.assertEqual(m1.shape, (m.nbytes,)) + self.assertEqual(m1.strides, (1,)) + self.assertTrue(m1.c_contiguous) + self.assertTrue(m1.contiguous) + self.assertEqual(m1.tobytes(), memoryview(nd).tobytes(order='F')) + + for fmt in ('B', 'b', 'c', 'H', 'I'): + size = struct.calcsize(fmt) + if m.nbytes % size == 0: + m2 = m.cast(fmt) + self.assertEqual(m2.ndim, 1) + self.assertEqual(m2.shape, (m.nbytes // size,)) + self.assertTrue(m2.contiguous) + + m3 = m[::-1] + with self.assertRaises(TypeError): + m3.cast('B') + def test_memoryview_tolist(self): # Most tolist() tests are in self.verify() etc. diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 1f701b3b7aa91f2..720b38cb508cbe6 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -2226,6 +2226,46 @@ def __len__(self): self.assertRaises(BufferError, ba.hex, S(b':')) + def test_no_init_called(self): + # A bytearray created without calling bytearray.__init__ + # should not crash the interpreter (see gh-153419). + def bytearray_new(): + return bytearray.__new__(bytearray) + + bytearray_new().insert(0, 1) + bytearray_new().extend(b"x") + bytearray_new().extend([1, 2, 3]) + bytearray_new().resize(4) + bytearray_new().__init__(5) + bytearray_new().__init__(b"xyz") + bytearray_new().take_bytes() + bytearray_new().take_bytes(0) + + a = bytearray_new() + a.append(1) + + a = bytearray_new() + a += b"x" + + a = bytearray_new() + a[:] = b"xyz" + + def test_reinit_length(self): + # There is a shortcut taken when resizing, where alloc/2 < newsize. + # In this case, the existing buffer is reused, rather than reset. + # If this happens when newsize == 0 and alloc == 1, then various + # code assumptions can be violated. This test should catch those + # in debug builds. (see gh-153419) + a = bytearray(1) + a.__init__() + self.assertEqual(a, b"") + + def test_reinit_with_view(self): + a = bytearray() + with memoryview(a): + self.assertRaises(BufferError, a.__init__, "x", "ascii") + self.assertEqual(a, b"") + class AssortedBytesTest(unittest.TestCase): # diff --git a/Lib/test/test_capi/test_capsule.py b/Lib/test/test_capi/test_capsule.py new file mode 100644 index 000000000000000..981caf3fad426bd --- /dev/null +++ b/Lib/test/test_capi/test_capsule.py @@ -0,0 +1,181 @@ +import importlib +import os +import sys +import textwrap +import unittest +from test.support import import_helper, os_helper + +_testlimitedcapi = import_helper.import_module('_testlimitedcapi') + + +class CapsuleImportTests(unittest.TestCase): + """Tests for PyCapsule_Import().""" + + @classmethod + def setUpClass(cls): + tmp = cls.tmp = cls.enterClassContext(os_helper.temp_dir()) + cls.enterClassContext(import_helper.DirsOnSysPath(tmp)) + cls.write_file(os.path.join(tmp, 'capsule_mod.py'), ''' + import _testlimitedcapi + + capsule = _testlimitedcapi.capsule_new('capsule_mod.capsule') + капсула = _testlimitedcapi.capsule_new('capsule_mod.капсула') + mismatched = _testlimitedcapi.capsule_new('other.name') + nonutf8 = _testlimitedcapi.capsule_new(b'capsule_mod.nonutf8\\xff') + nullname = _testlimitedcapi.capsule_new(None) + not_capsule = 42 + + class ns: + nested = _testlimitedcapi.capsule_new('capsule_mod.ns.nested') + + def __getattr__(name): + if name == 'bad_attr': + raise FloatingPointError('bad attribute') + raise AttributeError(name) + ''') + pkg = os.path.join(tmp, 'capsule_pkg') + os.mkdir(pkg) + cls.write_file(os.path.join(pkg, '__init__.py'), '') + cls.write_file(os.path.join(pkg, 'sub.py'), ''' + import _testlimitedcapi + + capsule = _testlimitedcapi.capsule_new('capsule_pkg.sub.capsule') + ''') + autopkg = os.path.join(tmp, 'capsule_autopkg') + os.mkdir(autopkg) + cls.write_file(os.path.join(autopkg, '__init__.py'), 'from . import sub\n') + cls.write_file(os.path.join(autopkg, 'sub.py'), ''' + import _testlimitedcapi + + capsule = _testlimitedcapi.capsule_new('capsule_autopkg.sub.capsule') + ''') + cls.write_file(os.path.join(tmp, 'capsule_broken.py'), '1/0\n') + importlib.invalidate_caches() + + def setUp(self): + for name in ('capsule_mod', 'capsule_pkg.sub', 'capsule_pkg', + 'capsule_autopkg.sub', 'capsule_autopkg', + 'capsule_broken'): + self.addCleanup(import_helper.unload, name) + + @staticmethod + def write_file(path, source): + with open(path, 'w', encoding='utf-8') as f: + f.write(textwrap.dedent(source)) + + def check_import(self, name, no_block=0): + # _testlimitedcapi.PyCapsule_Import() returns the name stored as the + # pointer by _testlimitedcapi.capsule_new(). + self.assertEqual(_testlimitedcapi.PyCapsule_Import(name, no_block), name) + + def test_import(self): + # The module is imported if not already imported. + self.assertNotIn('capsule_mod', sys.modules) + self.check_import('capsule_mod.capsule') + # Attributes after the first component are plain attribute lookups. + self.check_import('capsule_mod.ns.nested') + # Non-ASCII capsule and attribute name. + self.check_import('capsule_mod.капсула') + # The no_block argument is ignored. + self.check_import('capsule_mod.capsule', 1) + + @unittest.skipUnless(os_helper.TESTFN_NONASCII, + 'requires non-ASCII file name support') + def test_non_ascii_module_name(self): + name = os_helper.TESTFN_NONASCII + self.write_file(os.path.join(self.tmp, name + '.py'), f''' + import _testlimitedcapi + + capsule = _testlimitedcapi.capsule_new('{name}.capsule') + ''') + importlib.invalidate_caches() + self.addCleanup(import_helper.unload, name) + self.check_import(f'{name}.capsule') + + def test_submodule(self): + # Only the first component is imported; a submodule not imported + # by its package is not found. + self.assertRaises(AttributeError, + _testlimitedcapi.PyCapsule_Import, 'capsule_pkg.sub.capsule') + # It is found after explicit import. + importlib.import_module('capsule_pkg.sub') + self.check_import('capsule_pkg.sub.capsule') + # A submodule imported by its package is found. + self.check_import('capsule_autopkg.sub.capsule') + + def test_invalid_name(self): + pycapsule_import = _testlimitedcapi.PyCapsule_Import + # Non-existing module. + self.assertRaisesRegex(ImportError, + 'PyCapsule_Import could not import module "capsule_nonexistent"', + pycapsule_import, 'capsule_nonexistent.capsule') + # Non-UTF-8 module name. + self.assertRaisesRegex(ImportError, + 'PyCapsule_Import could not import module', + pycapsule_import, b'\xff\xfe.capsule') + # Empty module name. + self.assertRaisesRegex(ImportError, + 'PyCapsule_Import could not import module ""', + pycapsule_import, '.capsule_mod.capsule') + # Empty name. + self.assertRaisesRegex(ImportError, + 'PyCapsule_Import could not import module ""', + pycapsule_import, '') + # Only a dot. + self.assertRaisesRegex(ImportError, + 'PyCapsule_Import could not import module ""', + pycapsule_import, '.') + # Non-existing attribute. + self.assertRaises(AttributeError, + pycapsule_import, 'capsule_mod.nonexistent') + # Empty attribute name. + self.assertRaises(AttributeError, pycapsule_import, 'capsule_mod.') + # Consecutive dots. + self.assertRaises(AttributeError, + pycapsule_import, 'capsule_mod..capsule') + # Attribute of an object which is not a module. + self.assertRaises(AttributeError, + pycapsule_import, 'capsule_mod.not_capsule.capsule') + # No attribute name. + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod') + + # CRASHES pycapsule_import(NULL) + + def test_invalid_capsule(self): + pycapsule_import = _testlimitedcapi.PyCapsule_Import + # The attribute is not a capsule. + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.not_capsule') + # The capsule name does not match the requested name. + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.mismatched') + # The capsule name contains a byte not decodable from UTF-8. + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.nonutf8') + # Even the exactly matching name fails: the attribute lookup + # requires a name decodable from UTF-8. + self.assertRaises(UnicodeDecodeError, + pycapsule_import, b'capsule_mod.nonutf8\xff') + # The capsule name is NULL. + self.assertRaisesRegex(AttributeError, 'is not valid', + pycapsule_import, 'capsule_mod.nullname') + + def test_error_from_import(self): + # The exception raised during importing the module is replaced + # with generic ImportError. + with self.assertRaises(ImportError) as cm: + _testlimitedcapi.PyCapsule_Import('capsule_broken.capsule') + self.assertEqual(str(cm.exception), + 'PyCapsule_Import could not import ' + 'module "capsule_broken"') + + def test_error_from_attribute_lookup(self): + self.assertRaises(FloatingPointError, + _testlimitedcapi.PyCapsule_Import, 'capsule_mod.bad_attr') + self.assertRaises(FloatingPointError, + _testlimitedcapi.PyCapsule_Import, 'capsule_mod.bad_attr.capsule') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 7640e50f19b7839..25d6d1a248b4577 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -2,6 +2,7 @@ # Most tests are executed with environment variables ignored # See test_cmd_line_script.py for testing of script execution +import locale import os import re import subprocess @@ -369,9 +370,27 @@ def run_no_utf8_mode(arg): ) test_args = [valid_utf8, invalid_utf8] - for run_cmd in (run_default, run_c_locale, run_utf8_mode, - run_no_utf8_mode): - with self.subTest(run_cmd=run_cmd): + for run_cmd, encoding in ( + (run_default, sys.getfilesystemencoding()), + (run_c_locale, None), + (run_utf8_mode, None), + (run_no_utf8_mode, locale.getencoding()) + ): + with self.subTest(run_cmd=run_cmd.__name__): + # Arbitrary bytes round-trip through surrogateescape only in + # UTF-8 and single-byte encodings, not in a multibyte encoding + # such as EUC-JP. + if encoding is not None: + try: + lossless = len(bytes(range(256)).decode( + encoding, 'surrogateescape')) == 256 + except UnicodeError: + lossless = False + else: + lossless = True + if not lossless: + self.skipTest(f'{encoding} cannot losslessly ' + f'round-trip arbitrary bytes') for arg in test_args: proc = run_cmd(arg) self.assertEqual(proc.stdout.rstrip(), ascii(arg)) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index a04ed0c83c07c4a..1c723b25784da11 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -4147,6 +4147,15 @@ def test_float_operation_default(self): @requires_cdecimal class CContextFlags(ContextFlags, unittest.TestCase): decimal = C + + def test_signaldict_repr(self): + Context = self.decimal.Context + ctx = Context(prec=7) + mapping = ctx.flags + del ctx + with self.assertRaisesRegex(ValueError, 'invalid signal dict'): + repr(mapping) + class PyContextFlags(ContextFlags, unittest.TestCase): decimal = P diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index ee4888c18598f3d..0cee756958f3ad1 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -10,6 +10,7 @@ from codecs import BOM_UTF8 from itertools import product from textwrap import dedent +from types import ModuleType from test.support import (captured_stderr, check_impl_detail, cpython_only, gc_collect, @@ -2054,6 +2055,129 @@ def blech(self): self.assertEqual("bluch", exc.name) self.assertEqual(obj, exc.obj) + def test_getattr_error_message(self): + def fqn(type): + return f'{type.__module__}.{type.__qualname__}' + + class RaiseWithName: + def __getattr__(self, name): + raise AttributeError(name) + obj = RaiseWithName() + with self.assertRaises(AttributeError) as cm: + getattr(obj, "missing1") + self.assertEqual(str(cm.exception), + f"'{fqn(RaiseWithName)}' object has no attribute 'missing1'") + self.assertIs(cm.exception.obj, obj) + self.assertEqual(cm.exception.name, "missing1") + + class BareRaise: + def __getattr__(self, name): + raise AttributeError + obj = BareRaise() + with self.assertRaises(AttributeError) as cm: + getattr(obj, "missing2") + self.assertEqual(str(cm.exception), + f"'{fqn(BareRaise)}' object has no attribute 'missing2'") + self.assertIs(cm.exception.obj, obj) + self.assertEqual(cm.exception.name, "missing2") + + class RaiseCustom: + def __getattr__(self, name): + raise AttributeError("custom") + obj = RaiseCustom() + with self.assertRaises(AttributeError) as cm: + getattr(obj, "missing3") + self.assertEqual(str(cm.exception), "custom") + self.assertIs(cm.exception.obj, obj) + self.assertEqual(cm.exception.name, "missing3") + + def test_class_getattr_error_message(self): + def fqn(type): + return f'{type.__module__}.{type.__qualname__}' + + class MetaclassRaiseWithName(type): + def __getattr__(self, name): + raise AttributeError(name) + cls = MetaclassRaiseWithName("spam", (), {}) + with self.assertRaises(AttributeError) as cm: + getattr(cls, "missing1") + self.assertEqual(str(cm.exception), + f"type object '{fqn(cls)}' has no attribute 'missing1'") + self.assertIs(cm.exception.obj, cls) + self.assertEqual(cm.exception.name, "missing1") + + class MetaclassBareRaise(type): + def __getattr__(self, name): + raise AttributeError + cls = MetaclassBareRaise("eggs", (), {}) + with self.assertRaises(AttributeError) as cm: + getattr(cls, "missing2") + self.assertEqual(str(cm.exception), + f"type object '{fqn(cls)}' has no attribute 'missing2'") + self.assertIs(cm.exception.obj, cls) + self.assertEqual(cm.exception.name, "missing2") + + class MetaclassRaiseCustom(type): + def __getattr__(self, name): + raise AttributeError("custom") + cls = MetaclassRaiseCustom("ham", (), {}) + with self.assertRaises(AttributeError) as cm: + getattr(cls, "missing3") + self.assertEqual(str(cm.exception), "custom") + self.assertIs(cm.exception.obj, cls) + self.assertEqual(cm.exception.name, "missing3") + + def test_module_getattr_error_message(self): + raisewithname_mod = ModuleType("raisewithname") + def raise_with_name(name): + raise AttributeError(name) + raisewithname_mod.__getattr__ = raise_with_name + with self.assertRaises(AttributeError) as cm: + getattr(raisewithname_mod, "missing1") + self.assertEqual(str(cm.exception), + "module 'raisewithname' has no attribute 'missing1'") + self.assertIs(cm.exception.obj, raisewithname_mod) + self.assertEqual(cm.exception.name, "missing1") + + bareraise_mod = ModuleType("bareraise") + def bare_raise(name): + raise AttributeError + bareraise_mod.__getattr__ = bare_raise + with self.assertRaises(AttributeError) as cm: + getattr(bareraise_mod, "missing2") + self.assertEqual(str(cm.exception), + "module 'bareraise' has no attribute 'missing2'") + self.assertIs(cm.exception.obj, bareraise_mod) + self.assertEqual(cm.exception.name, "missing2") + + custom_mod = ModuleType("custom") + def raise_custom(name): + raise AttributeError("custom") + custom_mod.__getattr__ = raise_custom + with self.assertRaises(AttributeError) as cm: + getattr(custom_mod, "missing3") + self.assertEqual(str(cm.exception), "custom") + self.assertIs(cm.exception.obj, custom_mod) + self.assertEqual(cm.exception.name, "missing3") + + nameless_mod = ModuleType("forgettable") + del nameless_mod.__dict__["__name__"] + nameless_mod.__getattr__ = raise_with_name + with self.assertRaises(AttributeError) as cm: + getattr(nameless_mod, "missing4") + self.assertEqual(str(cm.exception), "module has no attribute 'missing4'") + self.assertIs(cm.exception.obj, nameless_mod) + self.assertEqual(cm.exception.name, "missing4") + + nameless_mod = ModuleType("broken") + nameless_mod.__dict__["__name__"] = 10j + nameless_mod.__getattr__ = raise_with_name + with self.assertRaises(AttributeError) as cm: + getattr(nameless_mod, "missing4") + self.assertEqual(str(cm.exception), "module has no attribute 'missing4'") + self.assertIs(cm.exception.obj, nameless_mod) + self.assertEqual(cm.exception.name, "missing4") + # Note: name suggestion tests live in `test_traceback`. diff --git a/Lib/test/test_gdb/test_pretty_print.py b/Lib/test/test_gdb/test_pretty_print.py index db3064e3df54c24..edb79ef86d31db5 100644 --- a/Lib/test/test_gdb/test_pretty_print.py +++ b/Lib/test/test_gdb/test_pretty_print.py @@ -114,29 +114,28 @@ def test_bytes(self): @support.requires_resource('cpu') def test_strings(self): 'Verify the pretty-printing of unicode strings' - # We cannot simply call locale.getpreferredencoding() here, - # as GDB might have been linked against a different version - # of Python with a different encoding and coercion policy - # with respect to PEP 538 and PEP 540. + # gdb emits its output in the host charset, which is not necessarily the + # getpreferredencoding() of the (possibly differently coerced) embedded + # Python. stdout, stderr = run_gdb( '--eval-command', - 'python import locale; print(locale.getpreferredencoding())') + 'python import gdb; print(gdb.host_charset())') - encoding = stdout + encoding = stdout.strip() if stderr or not encoding: raise RuntimeError( - f'unable to determine the Python locale preferred encoding ' - f'of embedded Python in GDB\n' + f'unable to determine the host charset of gdb\n' f'stdout={stdout!r}\n' f'stderr={stderr!r}') def check_repr(text): try: text.encode(encoding) - except UnicodeEncodeError: + # LookupError or ValueError if the host charset is unknown or invalid. + except (UnicodeEncodeError, LookupError, ValueError): self.assertGdbRepr(text, ascii(text)) else: - self.assertGdbRepr(text) + self.assertGdbRepr(text, repr(text).encode(encoding).decode('ascii', 'surrogateescape')) self.assertGdbRepr('') self.assertGdbRepr('And now for something hopefully the same') diff --git a/Lib/test/test_gdb/util.py b/Lib/test/test_gdb/util.py index d903adcf2903f34..29c7e9ee8f05498 100644 --- a/Lib/test/test_gdb/util.py +++ b/Lib/test/test_gdb/util.py @@ -78,7 +78,7 @@ def run_gdb(*args, exitcode=0, check=True, **env_vars): stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding="utf8", errors="backslashreplace", + encoding="ascii", errors="surrogateescape", env=env) stdout = proc.stdout diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 06b3aa66fc47a31..ccc7cce86883c89 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -2138,6 +2138,45 @@ def setUp(self): self.addCleanup(os_helper.unlink, self.address) SysLogHandlerTest.setUp(self) + def test_bytes_address(self): + # The Unix socket address can also be specified as bytes. + if self.server_exception: + self.skipTest(self.server_exception) + hdlr = logging.handlers.SysLogHandler(os.fsencode(self.address)) + self.addCleanup(hdlr.close) + self.assertTrue(hdlr.unixsocket) + logger = logging.getLogger("slh-bytes") + logger.addHandler(hdlr) + self.addCleanup(logger.removeHandler, hdlr) + logger.error("sp\xe4m") + self.handled.wait(support.LONG_TIMEOUT) + self.assertEqual(self.log_output, b'<11>sp\xc3\xa4m\x00') + +@unittest.skipUnless(sys.platform in ('linux', 'android'), + 'Linux specific test') +class AbstractNamespaceSysLogHandlerTest(BaseTest): + + """Test for SysLogHandler with a socket in the abstract namespace.""" + + def check(self, address): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.addCleanup(sock.close) + sock.bind(address) + sock.settimeout(support.LONG_TIMEOUT) + hdlr = logging.handlers.SysLogHandler(address) + self.addCleanup(hdlr.close) + self.assertTrue(hdlr.unixsocket) + hdlr.emit(logging.makeLogRecord({'msg': 'sp\xe4m'})) + self.assertEqual(sock.recv(1024), b'<12>sp\xc3\xa4m\x00') + + def test_str_address(self): + # A str address is encoded with the filesystem encoding. + self.check('\0' + os_helper.TESTFN) + + def test_bytes_address(self): + # The name is an arbitrary byte sequence, it need not be decodable. + self.check(b'\0test_logging_%d_\xff\xfe' % os.getpid()) + @unittest.skipUnless(socket_helper.IPV6_ENABLED, 'IPv6 support required for this test.') class IPv6SysLogHandlerTest(SysLogHandlerTest): @@ -3643,14 +3682,14 @@ def setup_via_listener(self, text, verify=None): # Ask for a randomly assigned port (by using port 0) t = logging.config.listen(0, verify) t.start() - t.ready.wait() + self.assertTrue(t.ready.wait(support.LONG_TIMEOUT), + msg='the listener did not start') # Now get the port allocated port = t.port t.ready.clear() try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(2.0) - sock.connect(('localhost', port)) + # The server can listen on IPv6, so do not force a family. + sock = socket.create_connection(('localhost', port), timeout=2.0) slen = struct.pack('>L', len(text)) s = slen + text @@ -3765,6 +3804,18 @@ def verify_reverse(stuff): ('ERROR', '2'), ], pat=r"^[\w.]+ -> (\w+): (\d+)$") + @support.requires_working_socket() + def test_listen_server_error(self): + # The "ready" event should be set even if the server fails to start. + t = logging.config.listen(-1) + t.daemon = True + with threading_helper.catch_threading_exception() as cm: + t.start() + self.assertTrue(t.ready.wait(support.SHORT_TIMEOUT), + msg='the listener did not report the failure') + threading_helper.join_thread(t) + self.assertIs(cm.exc_type, OverflowError) + def test_bad_format(self): self.assertRaises(ValueError, self.apply_config, self.bad_format) diff --git a/Lib/test/test_peg_generator/test_c_parser.py b/Lib/test/test_peg_generator/test_c_parser.py index 3500f229b1b3863..dc887693840a007 100644 --- a/Lib/test/test_peg_generator/test_c_parser.py +++ b/Lib/test/test_peg_generator/test_c_parser.py @@ -100,14 +100,17 @@ def setUpClass(cls): with contextlib.ExitStack() as stack: python_exe = stack.enter_context(support.setup_venv_with_pip_setuptools("venv")) - platlib_path = subprocess.check_output( - [python_exe, "-c", "import sysconfig; print(sysconfig.get_path('platlib'))"], - text=True, - ).strip() - purelib_path = subprocess.check_output( - [python_exe, "-c", "import sysconfig; print(sysconfig.get_path('purelib'))"], - text=True, - ).strip() + + def get_sysconfig_path(name): + # Force UTF-8 to emit the non-ASCII venv path in any locale. + return subprocess.check_output( + [python_exe, "-X", "utf8", "-c", + f"import sysconfig; print(sysconfig.get_path({name!r}))"], + encoding="utf-8", + ).strip() + + platlib_path = get_sysconfig_path("platlib") + purelib_path = get_sysconfig_path("purelib") stack.enter_context(import_helper.DirsOnSysPath(platlib_path, purelib_path)) cls.addClassCleanup(stack.pop_all().close) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-06-28-45.gh-issue-91484.huCgHt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-06-28-45.gh-issue-91484.huCgHt.rst new file mode 100644 index 000000000000000..7bba0da22d1c4da --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-15-06-28-45.gh-issue-91484.huCgHt.rst @@ -0,0 +1 @@ +:meth:`memoryview.cast` now allows casting from N-D to 1-D for F-contiguous. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst new file mode 100644 index 000000000000000..4459ec835395ea4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-01-17-57.gh-issue-153419.U8HJOJ.rst @@ -0,0 +1 @@ +Fix multiple :class:`bytearray` crashes and reference leaks caused by skipping :meth:`~object.__init__` and broken state setup code. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst new file mode 100644 index 000000000000000..40981acb37bbd70 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst @@ -0,0 +1,4 @@ +:exc:`AttributeError`: The default error message is now generated from ``name`` +and ``obj`` attributes when both are set and the exception was constructed with +no positional arguments, or with a single positional argument equal to ``name``. +Patch by Bartosz Sławecki. diff --git a/Misc/NEWS.d/next/Library/2025-10-19-17-34-07.gh-issue-140326.kZM0pV.rst b/Misc/NEWS.d/next/Library/2025-10-19-17-34-07.gh-issue-140326.kZM0pV.rst new file mode 100644 index 000000000000000..07eea563ea5fbc0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-19-17-34-07.gh-issue-140326.kZM0pV.rst @@ -0,0 +1,3 @@ +Fix the :mod:`asyncio` REPL namespace so that relative imports no longer +resolve against the :mod:`asyncio` package and ``__file__`` is no longer +set. diff --git a/Misc/NEWS.d/next/Library/2026-02-22-11-27-57.gh-issue-145107.ZlQXEZ.rst b/Misc/NEWS.d/next/Library/2026-02-22-11-27-57.gh-issue-145107.ZlQXEZ.rst new file mode 100644 index 000000000000000..c15afb53d6db4a6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-22-11-27-57.gh-issue-145107.ZlQXEZ.rst @@ -0,0 +1 @@ +Simplify ``asyncio.staggered_race`` by using ``eager_start=False``. diff --git a/Misc/NEWS.d/next/Library/2026-07-15-21-56-40.gh-issue-146011.nWmHif.rst b/Misc/NEWS.d/next/Library/2026-07-15-21-56-40.gh-issue-146011.nWmHif.rst new file mode 100644 index 000000000000000..0cac0257040bd6b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-15-21-56-40.gh-issue-146011.nWmHif.rst @@ -0,0 +1,2 @@ +Fix a heap-use-after-free in the C implementation of :mod:`decimal` +when calling :func:`repr` after deleting the :class:`~decimal.Context`. diff --git a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst new file mode 100644 index 000000000000000..2c6b07a4fa8b76f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst @@ -0,0 +1,3 @@ +Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread +data sitting in the FIFO made the transport misinterpret a poll event as +the reader disconnecting, wrongly closing the transport. diff --git a/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst new file mode 100644 index 000000000000000..27b6eeac5b520b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-13-01-24.gh-issue-73458.byxSPi.rst @@ -0,0 +1,5 @@ +Fix :func:`logging.config.listen`: it left the caller waiting for the +``ready`` event forever if the server could not be started, +for example if the port was invalid or already in use. +It now also binds to an IPv6 address if the host has no IPv4 address, +for example if ``localhost`` is only aliased to ``::1``. diff --git a/Misc/NEWS.d/next/Library/2026-07-22-17-35-24.gh-issue-70990.xVHKwt.rst b/Misc/NEWS.d/next/Library/2026-07-22-17-35-24.gh-issue-70990.xVHKwt.rst new file mode 100644 index 000000000000000..4af22f88ddc413f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-22-17-35-24.gh-issue-70990.xVHKwt.rst @@ -0,0 +1,4 @@ +:class:`logging.handlers.SysLogHandler` now accepts a :class:`bytes` address +of a Unix domain socket, including an address in the abstract namespace. +Previously only :class:`str` was recognized, +and a :class:`bytes` address raised :exc:`ValueError`. diff --git a/Misc/NEWS.d/next/Tests/2026-07-24-12-00-00.gh-issue-76595.KfF5xR.rst b/Misc/NEWS.d/next/Tests/2026-07-24-12-00-00.gh-issue-76595.KfF5xR.rst new file mode 100644 index 000000000000000..c344ee2f2cb30b7 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-07-24-12-00-00.gh-issue-76595.KfF5xR.rst @@ -0,0 +1 @@ +Add C API tests for :c:func:`PyCapsule_Import`. diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-07-24-09-30-00.gh-issue-154580.Kp3nQ2.rst b/Misc/NEWS.d/next/Tools-Demos/2026-07-24-09-30-00.gh-issue-154580.Kp3nQ2.rst new file mode 100644 index 000000000000000..f6a039245b015ed --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-07-24-09-30-00.gh-issue-154580.Kp3nQ2.rst @@ -0,0 +1,3 @@ +Fix ``python-gdb.py`` raising :exc:`UnicodeEncodeError` when pretty-printing a +non-ASCII :class:`str` in a locale whose host charset cannot encode it, such as +any non-ASCII string in the C locale. diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 0f88dddf03b9f68..eaf8777a56c59b6 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c -@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c +@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index dc1b3c06bed9521..6fbce0ed85e695d 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -1499,6 +1499,20 @@ static int context_clear(PyObject *op) { PyDecContextObject *self = _PyDecContextObject_CAST(op); + /* Since traps and flags hold a borrowed reference to the + flags stored in the context object, these references need + to be cleared when the context object is deallocated + because traps and flags can survive. See gh-146011. */ + PyDecSignalDictObject *traps = _PyDecSignalDictObject_CAST(self->traps); + PyDecSignalDictObject *flags = _PyDecSignalDictObject_CAST(self->flags); + + if (traps != NULL) { + traps->flags = NULL; + } + if (flags != NULL) { + flags->flags = NULL; + } + Py_CLEAR(self->traps); Py_CLEAR(self->flags); return 0; diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index 8ce704502af0104..0a562ea9c03110b 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -38,6 +38,9 @@ PyInit__testlimitedcapi(void) if (_PyTestLimitedCAPI_Init_Bytes(mod) < 0) { return NULL; } + if (_PyTestLimitedCAPI_Init_Capsule(mod) < 0) { + return NULL; + } if (_PyTestLimitedCAPI_Init_Codec(mod) < 0) { return NULL; } diff --git a/Modules/_testlimitedcapi/capsule.c b/Modules/_testlimitedcapi/capsule.c new file mode 100644 index 000000000000000..07b85823f399ba3 --- /dev/null +++ b/Modules/_testlimitedcapi/capsule.c @@ -0,0 +1,63 @@ +#include "parts.h" + +static void +capsule_destructor(PyObject *op) +{ + /* If non-NULL, the name and the pointer are the same allocation. */ + free((char *)PyCapsule_GetName(op)); +} + +static PyObject * +capsule_new(PyObject *self, PyObject *arg) +{ + const char *name; + Py_ssize_t size; + if (!PyArg_Parse(arg, "z#", &name, &size)) { + return NULL; + } + char *name_copy = NULL; + if (name != NULL) { + name_copy = strdup(name); + if (name_copy == NULL) { + return PyErr_NoMemory(); + } + } + static const char dummy = 0; + void *pointer = name_copy != NULL ? (void *)name_copy : (void *)&dummy; + PyObject *capsule = PyCapsule_New(pointer, name_copy, capsule_destructor); + if (capsule == NULL) { + free(name_copy); + } + return capsule; +} + +static PyObject * +pycapsule_import(PyObject *self, PyObject *args) +{ + const char *name; + Py_ssize_t size; + int no_block = 0; + if (!PyArg_ParseTuple(args, "z#|i", &name, &size, &no_block)) { + return NULL; + } + void *pointer = PyCapsule_Import(name, no_block); + if (pointer == NULL) { + return NULL; + } + /* Capsules created by capsule_new() store a copy of their name as the + pointer, so a successful import round-trips the name. Only use this + function with such capsules. */ + return PyUnicode_FromString((const char *)pointer); +} + +static PyMethodDef test_methods[] = { + {"capsule_new", capsule_new, METH_O}, + {"PyCapsule_Import", pycapsule_import, METH_VARARGS}, + {NULL}, +}; + +int +_PyTestLimitedCAPI_Init_Capsule(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testlimitedcapi/parts.h b/Modules/_testlimitedcapi/parts.h index a11e0edb8311e31..32c1bbc1b71c977 100644 --- a/Modules/_testlimitedcapi/parts.h +++ b/Modules/_testlimitedcapi/parts.h @@ -25,6 +25,7 @@ int _PyTestLimitedCAPI_Init_Abstract(PyObject *module); int _PyTestLimitedCAPI_Init_ByteArray(PyObject *module); int _PyTestLimitedCAPI_Init_Bytes(PyObject *module); +int _PyTestLimitedCAPI_Init_Capsule(PyObject *module); int _PyTestLimitedCAPI_Init_Codec(PyObject *module); int _PyTestLimitedCAPI_Init_Complex(PyObject *module); int _PyTestLimitedCAPI_Init_Dict(PyObject *module); diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index e5db77ce14f31be..d009877dc09fac0 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -213,6 +213,9 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) { _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyByteArrayObject *obj = ((PyByteArrayObject *)self); + + assert(obj->ob_bytes_object != NULL); + /* All computations are done unsigned to avoid integer overflows (see issue #22335). */ size_t alloc = (size_t) obj->ob_alloc; @@ -236,6 +239,14 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size) return -1; } + /* Resize to 0 resets to empty bytes (see issue #153419). */ + if (requested_size == 0) { + Py_SETREF(obj->ob_bytes_object, + Py_GetConstant(Py_CONSTANT_EMPTY_BYTES)); + bytearray_reinit_from_bytes(obj, 0, 0); + return 0; + } + if (size + logical_offset <= alloc) { /* Current buffer is large enough to host the requested size, decide on a strategy. */ @@ -902,6 +913,20 @@ bytearray_ass_subscript(PyObject *op, PyObject *index, PyObject *values) return ret; } +static PyObject * +bytearray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + PyObject *op = PyType_GenericNew(type, args, kwds); + if (op == NULL) { + return NULL; + } + PyByteArrayObject *self = _PyByteArray_CAST(op); + self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); + bytearray_reinit_from_bytes(self, 0, 0); + self->ob_exports = 0; + return op; +} + /*[clinic input] bytearray.__init__ @@ -920,20 +945,16 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg, PyObject *it; PyObject *(*iternext)(PyObject *); - /* First __init__; set ob_bytes_object so ob_bytes is always non-null. */ - if (self->ob_bytes_object == NULL) { - self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); - bytearray_reinit_from_bytes(self, 0, 0); - self->ob_exports = 0; + /* Disallow any __init__ call if the object is not resizable (has exports) + to make the handling of non-null `source` init values simpler. */ + if (!_canresize(self)) { + return -1; } - if (Py_SIZE(self) != 0) { - /* Empty previous contents (yes, do this first of all!) */ - if (PyByteArray_Resize((PyObject *)self, 0) < 0) - return -1; + /* Empty any previous contents (do this first of all!). */ + if (PyByteArray_Resize((PyObject *)self, 0) < 0) { + return -1; } - - /* Should be caused by first init or the resize to 0. */ assert(self->ob_bytes_object == Py_GetConstantBorrowed(Py_CONSTANT_EMPTY_BYTES)); assert(self->ob_exports == 0); @@ -1609,6 +1630,9 @@ bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n) } if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) { + assert(self->ob_bytes_object == NULL); + self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); + bytearray_reinit_from_bytes(self, 0, 0); Py_DECREF(remaining); return NULL; } @@ -2939,7 +2963,7 @@ PyTypeObject PyByteArray_Type = { 0, /* tp_dictoffset */ bytearray___init__, /* tp_init */ PyType_GenericAlloc, /* tp_alloc */ - PyType_GenericNew, /* tp_new */ + bytearray_new, /* tp_new */ PyObject_Free, /* tp_free */ .tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY, }; diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c index f63185e14284b1a..ef35dad82e8aaea 100644 --- a/Objects/bytesobject.c +++ b/Objects/bytesobject.c @@ -3380,6 +3380,7 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize) Py_DECREF(v); return (*pv == NULL) ? -1 : 0; } + assert(v != bytes_get_empty()); #ifdef Py_TRACE_REFS _Py_ForgetReference(v); diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 03036020b1cb1ae..d7955cc7390a7ab 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -748,6 +748,7 @@ _PyCode_New(struct _PyCodeConstructor *con) #endif if (init_code(co, con) < 0) { + Py_XDECREF(replacement_locations); Py_DECREF(co); return NULL; } diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 149595e64cec144..fb546ad2673576d 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -11,9 +11,11 @@ #include "pycore_exceptions.h" // struct _Py_exc_state #include "pycore_initconfig.h" #include "pycore_modsupport.h" // _PyArg_NoKeywords() +#include "pycore_moduleobject.h" // _PyModule_CAST() #include "pycore_object.h" #include "pycore_pyerrors.h" // struct _PyErr_SetRaisedException #include "pycore_tuple.h" // _PyTuple_FromPair +#include "pycore_unicodeobject.h" // _PyUnicode_Equal() #include "osdefs.h" // SEP #include "clinic/exceptions.c.h" @@ -2702,6 +2704,65 @@ AttributeError_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } +static PyObject * +AttributeError_str(PyObject *op) +{ + PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); + PyObject *arg; // borrowed ref + PyObject *obj = NULL, *name = NULL; + + /* .name and .obj are set automatically when attribute lookup fails, so + synthesize a more informative message from them when the caller + didn't supply a meaningful one of their own -- that is, when args is + empty, or contains only the attribute name. Otherwise, use the + message the caller gave. */ + + Py_BEGIN_CRITICAL_SECTION(self); + if ( + self->obj && self->name && PyUnicode_Check(self->name) + && ((PyTuple_GET_SIZE(self->args) == 1 + && PyUnicode_Check(arg = PyTuple_GET_ITEM(self->args, 0)) + && _PyUnicode_Equal(arg, self->name)) + || PyTuple_GET_SIZE(self->args) == 0) + ) { + obj = Py_NewRef(self->obj); + name = Py_NewRef(self->name); + } + Py_END_CRITICAL_SECTION(); + + if (!obj) { + assert(!name); + return BaseException_str(op); /* re-acquires lock */ + } + + PyObject *result = NULL; + if (PyModule_Check(obj)) { + PyModuleObject *mod = _PyModule_CAST(obj); + PyObject *modname; + if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__name__), &modname) < 0) { + goto done; + } + if (modname && PyUnicode_Check(modname)) { + result = PyUnicode_FromFormat("module %R has no attribute %R", + modname, name); + Py_DECREF(modname); + } else { + Py_XDECREF(modname); + result = PyUnicode_FromFormat("module has no attribute %R", name); + } + } else if (PyType_Check(obj)) { + result = PyUnicode_FromFormat("type object '%N' has no attribute %R", + obj, name); + } else { + result = PyUnicode_FromFormat("'%T' object has no attribute %R", + obj, name); + } +done: + Py_DECREF(obj); + Py_DECREF(name); + return result; +} + static int AttributeError_traverse(PyObject *op, visitproc visit, void *arg) { @@ -2770,7 +2831,7 @@ static PyMethodDef AttributeError_methods[] = { ComplexExtendsException(PyExc_Exception, AttributeError, AttributeError, 0, AttributeError_methods, AttributeError_members, - 0, BaseException_str, 0, "Attribute not found."); + 0, AttributeError_str, 0, "Attribute not found."); /* * SyntaxError extends Exception diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index d92c7daff15dbf3..f6ffa337c632a0b 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1485,9 +1485,11 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, CHECK_RESTRICTED(self); if (!MV_C_CONTIGUOUS(self->flags)) { - PyErr_SetString(PyExc_TypeError, - "memoryview: casts are restricted to C-contiguous views"); - return NULL; + if (shape || !MV_F_CONTIGUOUS(self->flags)) { + PyErr_SetString(PyExc_TypeError, + "memoryview: casts are restricted to contiguous views"); + return NULL; + } } if ((shape || self->view.ndim != 1) && zero_in_shape(self)) { PyErr_SetString(PyExc_TypeError, diff --git a/PCbuild/_testlimitedcapi.vcxproj b/PCbuild/_testlimitedcapi.vcxproj index 7ddcee6d9735ce6..785bb151e081293 100644 --- a/PCbuild/_testlimitedcapi.vcxproj +++ b/PCbuild/_testlimitedcapi.vcxproj @@ -97,6 +97,7 @@ + diff --git a/PCbuild/_testlimitedcapi.vcxproj.filters b/PCbuild/_testlimitedcapi.vcxproj.filters index 66a0a47d8e5548b..51dc9950a103769 100644 --- a/PCbuild/_testlimitedcapi.vcxproj.filters +++ b/PCbuild/_testlimitedcapi.vcxproj.filters @@ -12,6 +12,7 @@ + diff --git a/Tools/gdb/libpython.py b/Tools/gdb/libpython.py index ba52ea2a30e0be1..422e4f605920a0c 100755 --- a/Tools/gdb/libpython.py +++ b/Tools/gdb/libpython.py @@ -43,7 +43,6 @@ import gdb import os -import locale import sys @@ -107,8 +106,6 @@ def interp_frame_has_tlbc_index(): USED_TAGS = 0b11 -ENCODING = locale.getpreferredencoding() - FRAME_INFO_OPTIMIZED_OUT = '(frame information optimized out)' UNABLE_READ_INFO_PYTHON_FRAME = 'Unable to read information on python frame' EVALFRAME = '_PyEval_EvalFrameDefault' @@ -1504,6 +1501,10 @@ def proxyval(self, visited): def write_repr(self, out, visited): # Write this out as a Python str literal + # gdb writes its output in the host charset, so a character is escaped + # unless it is printable and encodable in that charset. + encoding = gdb.host_charset() + # Get a PyUnicodeObject* within the Python gdb process: proxy = self.proxyval(visited) @@ -1551,8 +1552,10 @@ def write_repr(self, out, visited): printable = ucs.isprintable() if printable: try: - ucs.encode(ENCODING) - except UnicodeEncodeError: + ucs.encode(encoding) + # LookupError or ValueError if the host charset is unknown + # or invalid. + except (UnicodeEncodeError, LookupError, ValueError): printable = False # Map Unicode whitespace and control characters