From b633787e11a672ea73dbab6f152e25a9dbed45e2 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Fri, 17 Apr 2026 14:18:25 +0200 Subject: [PATCH 1/6] Enumerations: Enable the forgiveness feature ENOPT_NO_ZERODEFAULT for 3.15 Adapt the op codes for detecting a parameterless __init__() call in enum code to 3.15. Task-number: PYSIDE-3221 Task-number: PYSIDE-1735 Change-Id: I21b8b8108e2d74e13ff4cb0f7fe2f41121361422 Reviewed-by: Shyamnath Premnadh Reviewed-by: Ece Cinucen --- a/sources/pyside6/tests/pysidetest/enum_test.py +++ b/sources/pyside6/tests/pysidetest/enum_test.py @@ -209,8 +209,27 @@ class InvestigateOpcodesTest(unittest.TestCase): ('LOAD_CONST', 82, 0), ('RETURN_VALUE', 35, None)] - self.assertEqual(self.read_code(self.probe_function1), result_1) - self.assertEqual(self.read_code(self.probe_function2), result_2) + if sys.version_info[:2] >= (3, 15): + result_1 = [('RESUME', 128, 0), + ('LOAD_GLOBAL', 91, 0), + ('LOAD_ATTR', 79, 2), + ('STORE_FAST', 111, 1), + ('LOAD_CONST', 81, 0), + ('RETURN_VALUE', 33, None) + ] + result_2 = [('RESUME', 128, 0), + ('LOAD_GLOBAL', 91, 0), + ('LOAD_ATTR', 79, 2), + ('PUSH_NULL', 31, None), + ('CALL', 50, 0), + ('STORE_FAST', 111, 1), + ('LOAD_CONST', 81, 0), + ('RETURN_VALUE', 33, None)] + + actual1 = self.read_code(self.probe_function1) + actual2 = self.read_code(self.probe_function2) + self.assertEqual(actual1, result_1) + self.assertEqual(actual2, result_2) if __name__ == '__main__': --- a/sources/shiboken6/libshiboken/sbkfeature_base.cpp +++ b/sources/shiboken6/libshiboken/sbkfeature_base.cpp @@ -114,10 +114,12 @@ void disassembleFrame(const char *marker) // OpCodes: Adapt for each Python version by checking the defines in the generated header opcode_ids.h // egrep '( LOAD_ATTR | CALL | PUSH_NULL )' opcode_ids.h -// See also test sources/pyside6/tests/pysidetest/enum_test.py +// See also test sources/pyside6/tests/pysidetest/enum_test.py, sbkmodule.cpp:300 static int constexpr LOAD_ATTR_OpCode(long pyVersion) { + if (pyVersion >= 0x030F00) // 3.15 + return 79; if (pyVersion >= 0x030E00) // 3.14 return 80; if (pyVersion >= 0x030D00) // 3.13 @@ -127,6 +129,8 @@ static int constexpr LOAD_ATTR_OpCode(long pyVersion) static int constexpr CALL_OpCode(long pyVersion) { + if (pyVersion >= 0x030F00) // 3.15 + return 50; if (pyVersion >= 0x030E00) // 3.14 return 52; if (pyVersion >= 0x030D00) // 3.13 @@ -136,6 +140,8 @@ static int constexpr CALL_OpCode(long pyVersion) static int constexpr PUSH_NULL_OpCode(long pyVersion) { + if (pyVersion >= 0x030F00) // 3.15 + return 31; if (pyVersion >= 0x030E00) // 3.14 return 33; return 34; --- a/sources/shiboken6/libshiboken/sbkmodule.cpp +++ b/sources/shiboken6/libshiboken/sbkmodule.cpp @@ -309,6 +309,8 @@ static int constexpr LOAD_CONST_OpCode(long pyVersion) static int constexpr IMPORT_NAME_OpCode(long pyVersion) { + if (pyVersion >= 0x030F00) // 3.15 + return 72; if (pyVersion >= 0x030E00) // 3.14 return 73; if (pyVersion >= 0x030D00) // 3.13 -- 2.56.0 From dc5fb42358a129c9f2ebf9c8f155f13870793803 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Mon, 20 Apr 2026 08:59:12 +0200 Subject: [PATCH 2/6] Fix the star import test for 3.15 Adapt the op codes for star import detection and adapt the oparg2 parameter handling to cpython/46d5106cfa903329821c097c3bf309e3efcd718f (lazy import freature in 3.15). Task-number: PYSIDE-3221 Task-number: PYSIDE-1735 Change-Id: Ief2a742c21d5073d5fae7ae409f0779cdd6adb77 Reviewed-by: Shyamnath Premnadh --- a/sources/shiboken6/libshiboken/sbkmodule.cpp +++ b/sources/shiboken6/libshiboken/sbkmodule.cpp @@ -297,9 +297,12 @@ static PyMethodDef module_methods[] = { // OpCodes: Adapt for each Python version by checking the defines in the generated header opcode_ids.h // egrep '( LOAD_CONST | IMPORT_NAME )' opcode_ids.h +// See also sbkfeature_base.cpp:115 static int constexpr LOAD_CONST_OpCode(long pyVersion) { + if (pyVersion >= 0x030F00) // 3.15 + return 81; if (pyVersion >= 0x030E00) // 3.14 return 82; if (pyVersion >= 0x030D00) // 3.13 @@ -361,6 +364,10 @@ static bool detectImportStar(PyObject *dec_frame, PyObject *module) } Py_ssize_t oparg2 = uint8_t(co_code[f_lasti + 1]); + // Cf cpython/46d5106cfa903329821c097c3bf309e3efcd718f, Lazy import in 3.15, + //cpython/Python/generated_cases.c.h:6410 + if (pyVersion >= 0x030F00) // 3.15 + oparg2 >>= 2; AutoDecRef dec_co_names(PyObject_GetAttr(dec_f_code, _co_names)); if (dec_co_names.isNull() || PyTuple_Check(dec_co_names.object()) == 0 || oparg2 >= PyTuple_Size(dec_co_names.object())) { -- 2.56.0 From 606369c2c2383fa49ee86902cad8149ef7d449bd Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Mon, 15 Jun 2026 09:00:08 +0200 Subject: [PATCH 3/6] enum_test.py: Adapt to 3.15.0b2+dev Adapt the op codes used to check for enumerations. Complements ccdba08a37998bbbabadf42abfae871af5df8ac3. Task-number: PYSIDE-3221 Task-number: PYSIDE-1735 Change-Id: I3ac4d397299d32ab2e49a00f7165659792a04092 Reviewed-by: Cristian Maureira-Fredes --- a/sources/pyside6/tests/pysidetest/enum_test.py +++ b/sources/pyside6/tests/pysidetest/enum_test.py @@ -214,7 +214,7 @@ class InvestigateOpcodesTest(unittest.TestCase): ('LOAD_GLOBAL', 91, 0), ('LOAD_ATTR', 79, 2), ('STORE_FAST', 111, 1), - ('LOAD_CONST', 81, 0), + ('LOAD_COMMON_CONSTANT', 80, 7), ('RETURN_VALUE', 33, None) ] result_2 = [('RESUME', 128, 0), @@ -223,7 +223,7 @@ class InvestigateOpcodesTest(unittest.TestCase): ('PUSH_NULL', 31, None), ('CALL', 50, 0), ('STORE_FAST', 111, 1), - ('LOAD_CONST', 81, 0), + ('LOAD_COMMON_CONSTANT', 80, 7), ('RETURN_VALUE', 33, None)] actual1 = self.read_code(self.probe_function1) -- 2.56.0 From c66af962f1e3e341de858436f64285cdbebc44c0 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Tue, 13 Jan 2026 10:13:55 +0100 Subject: [PATCH 4/6] Bump Python supported version to 3.15 Task-number: PYSIDE-3221 Change-Id: I0b3d7bb5bb1c4857c8d37551df587eaf80111c6a Reviewed-by: Shyamnath Premnadh --- a/wheel_artifacts/pyproject.toml.base +++ b/wheel_artifacts/pyproject.toml.base @@ -10,7 +10,7 @@ authors = [ description = "PROJECT_DESCRIPTION" readme = "PROJECT_README" dynamic = ["version"] -requires-python = ">=3.10, <3.15" +requires-python = ">=3.10, <3.16" keywords = ["Qt"] license = {text="LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only"} dependencies = "PROJECT_DEPENDENCIES" @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Database", "Topic :: Software Development", "Topic :: Software Development :: Code Generators", -- 2.56.0 From 83da7eb97a6f9b8463f5b277b4944dd037f3ad5e Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Thu, 17 Sep 2026 13:41:51 +0200 Subject: [PATCH 5/6] Fix QtAsyncio to work with Python 3.15 Import class ThreadPoolExecutor directly, fixing: File ".../pyside6/PySide6/QtAsyncio/events.py", line 213, in __init__ self._default_executor = concurrent.futures.ThreadPoolExecutor() ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ TypeError: 'lazy_import' object is not callable Task-number: PYSIDE-3221 Change-Id: I26a5730d6bbd15114c6deb17e468db6778513708 Reviewed-by: Cristian Maureira-Fredes --- a/sources/pyside6/PySide6/QtAsyncio/events.py +++ b/sources/pyside6/PySide6/QtAsyncio/events.py @@ -13,7 +13,7 @@ from typing import Any, Callable, TypeVar import asyncio import collections.abc -import concurrent.futures +from concurrent.futures import ThreadPoolExecutor import contextvars import enum import os @@ -210,7 +210,7 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): # Starting with Python 3.11, this must be an instance of # ThreadPoolExecutor. - self._default_executor = concurrent.futures.ThreadPoolExecutor() + self._default_executor = ThreadPoolExecutor() # The exception handler, if set with set_exception_handler(). The # exception handler is currently called in two places: One, if an @@ -576,7 +576,7 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): # Executing code in thread or process pools - def run_in_executor(self, executor: concurrent.futures.ThreadPoolExecutor | None, + def run_in_executor(self, executor: ThreadPoolExecutor | None, func: Callable[[Unpack[_Ts]], _T], *args: Unpack[_Ts]) -> asyncio.Future[_T]: if self.is_closed(): @@ -594,8 +594,8 @@ class QAsyncioEventLoop(asyncio.BaseEventLoop, QObject): return asyncio.futures.wrap_future(executor.submit(wrapper.do), loop=self) def set_default_executor(self, - executor: concurrent.futures.ThreadPoolExecutor | None) -> None: - if not isinstance(executor, concurrent.futures.ThreadPoolExecutor): + executor: ThreadPoolExecutor | None) -> None: + if not isinstance(executor, ThreadPoolExecutor): raise TypeError("The executor must be a ThreadPoolExecutor") self._default_executor = executor -- 2.56.0 From 88c7baba4ff611331b094a615a485f151e92d5f1 Mon Sep 17 00:00:00 2001 From: Friedemann Kleint Date: Mon, 5 Oct 2026 08:56:12 +0200 Subject: [PATCH 6/6] Fix Python 3.15 lazy imports to work with PySide lazy imports Move the check whether the module in questions is ours to the top since PyObject_GenericGetAttr() no longer works in the case of lazy import proxies. Fixes: PYSIDE-3471 Task-number: PYSIDE-3221 Change-Id: Iff35d8ece0f3ce239545cc767e73b27e0e32ef14 Reviewed-by: Cristian Maureira-Fredes (cherry picked from commit 43ad3e3656b09bbe7f33f2463e6a2912c3c31188) Reviewed-by: Qt Cherry-pick Bot --- a/sources/pyside6/tests/QtAsyncio/CMakeLists.txt +++ b/sources/pyside6/tests/QtAsyncio/CMakeLists.txt @@ -5,6 +5,7 @@ PYSIDE_TEST(qasyncio_test_cancel_task.py) PYSIDE_TEST(qasyncio_test_cancel_taskgroup.py) PYSIDE_TEST(qasyncio_test_chain.py) PYSIDE_TEST(qasyncio_test_executor.py) +PYSIDE_TEST(qasyncio_lazy_import.py) PYSIDE_TEST(qasyncio_test_queues.py) PYSIDE_TEST(qasyncio_test_threadsafe.py) PYSIDE_TEST(qasyncio_test_time.py) --- /dev/null +++ b/sources/pyside6/tests/QtAsyncio/qasyncio_lazy_import.py @@ -0,0 +1,31 @@ +# Copyright (C) 2026 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +'''Test cases for QtAsyncio''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths # noqa: E402 +init_test_paths(True) + +import shiboken6 # noqa: F401, E402 +import asyncio # noqa: E402 + + +class QAsyncioLazyImportTestCase(unittest.TestCase): + """PYSIDE-3471: Verify that the libshiboken's module import hook for lazy class + loading does not interfere with the lazy import introduced in Python 3.15, + which results in a lazy import being returned for concurrent.futures.ThreadPoolExecutor + in the asyncio code.""" + def test(self): + v = asyncio.run(asyncio.to_thread(lambda: 42)) + self.assertEqual(v, 42) + + +if __name__ == '__main__': + unittest.main() --- a/sources/shiboken6/libshiboken/sbkmodule.cpp +++ b/sources/shiboken6/libshiboken/sbkmodule.cpp @@ -238,18 +238,21 @@ static getattrofunc origModuleGetattro{}; // This modifies _all_ modules but should have no impact. static PyObject *PyModule_lazyGetAttro(PyObject *module, PyObject *name) { - // - check if the attribute is present and return it. + // - locate the module in the moduleTofuncs mapping + auto tableIter = moduleToFuncs.find(module); + // - if this is not our module, use the original. As of 3.15, this needs to be + // done first since PyObject_GenericGetAttr() no longer works in the case + // of lazy import proxies (PYSIDE-3471). + if (tableIter == moduleToFuncs.end()) + return origModuleGetattro(module, name); + + // - check if the attribute is present (non-class items) and return it. auto *attr = PyObject_GenericGetAttr(module, name); // - we handle AttributeError, only. if (attr != nullptr || PyErr_ExceptionMatches(PyExc_AttributeError) == 0) return attr; PyErr_Clear(); - // - locate the module in the moduleTofuncs mapping - auto tableIter = moduleToFuncs.find(module); - // - if this is not our module, use the original - if (tableIter == moduleToFuncs.end()) - return origModuleGetattro(module, name); // - locate the name and retrieve the generating function const char *attrNameStr = Shiboken::String::toCString(name); -- 2.56.0