Package: src:python-bytecode
Version: 0.17.0-1
User: [email protected]
Usertags: python3.15
Tags: patch, ftbfs, forky, sid
Severity: important

Hi!

While rebuilding the python related packages against the Python 3.15rc2
version we found that python-bytecode fails to build from source [1].
The problem can be solved with upstream commit
4bff24f022bb2c6bfe1c368821180534140ce0bb: feat: add support for CPython
3.15 [2].

I've applied the upstream fix in the sandbox [3] to verify that it
builds successfully, please consider applying the patch to support the
upcoming 3.15 version.

Setting the severity to important for now. Once Python 3.15 is released,
it will be added to python3-defaults and this bug will become release
critical.

Happy hacking,

[1]: https://debusine.debian.net/debian/r-python-python3.15/artifact/4480850/
[2]: 
https://github.com/MatthieuDartiailh/bytecode/commit/4bff24f022bb2c6bfe1c368821180534140ce0bb
[3]: https://debusine.debian.net/debian/r-python-python3.15/

--
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
From: Gabriele N. Tornetta <[email protected]>
Date: Fri, 21 Aug 2026 22:02:56 +0100
Subject: feat: add support for CPython 3.15 (#192)

* feat: add support for CPython 3.15

* beta1 updates

* add fallback version for shallow clones

* fix typing

* add 3.15 framework tests run

* IMPORT_NAME fix

* address review comments

---------

Co-authored-by: Matthieu Dartiailh <[email protected]>
Origin: upstream, https://github.com/MatthieuDartiailh/bytecode/commit/4bff24f022bb2c6bfe1c368821180534140ce0bb

Index: python-bytecode/src/bytecode/instr.py
===================================================================
--- python-bytecode.orig/src/bytecode/instr.py
+++ python-bytecode/src/bytecode/instr.py
@@ -13,7 +13,7 @@ except ImportError:
     from typing_extensions import TypeGuard  # type: ignore
 
 import bytecode as _bytecode
-from bytecode.utils import PY311, PY312, PY313, PY314
+from bytecode.utils import PY311, PY312, PY313, PY314, PY315
 
 # --- Instruction argument tools and
 
@@ -35,7 +35,11 @@ BITFLAG_OPCODES = (
     )
 )
 
-BITFLAG2_OPCODES = (_opcode.opmap["LOAD_SUPER_ATTR"],) if PY312 else ()
+BITFLAG2_OPCODES = (
+    (_opcode.opmap["LOAD_SUPER_ATTR"], _opcode.opmap["IMPORT_NAME"])
+    if PY315
+    else ((_opcode.opmap["LOAD_SUPER_ATTR"],) if PY312 else ())
+)
 
 # Binary op opcode which has a dedicated arg
 BINARY_OPS = (_opcode.opmap["BINARY_OP"],) if PY311 else ()
@@ -48,6 +52,9 @@ INTRINSIC = INTRINSIC_1OP + INTRINSIC_2O
 # Small integer related opcode
 SMALL_INT_OPS = (_opcode.opmap["LOAD_SMALL_INT"],) if PY314 else ()
 
+# Opcodes that gained a cache-only argument in 3.15 (arg is always 0 and not user-visible)
+CACHE_ONLY_ARG_OPCODES = (_opcode.opmap["GET_ITER"],) if PY315 else ()
+
 # Special method loading related opcodes
 SPECIAL_OPS = (_opcode.opmap["LOAD_SPECIAL"],) if PY314 else ()
 
@@ -218,6 +225,15 @@ class CommonConstant(enum.IntEnum):
     BUILTIN_ALL = 3
     BUILTIN_ANY = 4
 
+    if PY315:
+        BUILTIN_LIST = 5
+        BUILTIN_SET = 6
+        CONSTANT_NONE = 7
+        CONSTANT_EMPTY_STR = 8
+        CONSTANT_TRUE = 9
+        CONSTANT_FALSE = 10
+        CONSTANT_MINUS_ONE = 11
+
 
 # This make type checking happy but means it won't catch attempt to manipulate an unset
 # statically. We would need guard on object attribute narrowed down through methods
@@ -368,7 +384,7 @@ STATIC_STACK_EFFECTS: Dict[str, Tuple[in
     "DUP_TOP": (-1, 2),
     "DUP_TOP_TWO": (-2, 4),
     "GET_LEN": (-1, 2),
-    "GET_ITER": (-1, 1),
+    "GET_ITER": (-1, 2) if PY315 else (-1, 1),
     "GET_YIELD_FROM_ITER": (-1, 1),
     "GET_AWAITABLE": (-1, 1),
     "GET_AITER": (-1, 1),
@@ -448,7 +464,14 @@ DYNAMIC_STACK_EFFECTS: Dict[
     "MAP_ADD": lambda effect, arg, jump: (-arg, arg - 2),
     "FORMAT_VALUE": lambda effect, arg, jump: (effect - 1, 1),
     # FOR_ITER needs TOS to be an iterator, hence a prerequisite of 1 on the stack
-    "FOR_ITER": lambda effect, arg, jump: (effect, 0) if jump else (-1, 2),
+    # In 3.15, GET_ITER pushes (iter, null_or_index) as two stack slots, so
+    # FOR_ITER now requires both on the stack (-2) and always pushes them back
+    # plus one more slot (+3): the next value when continuing, or a marker
+    # consumed by END_FOR when exhausted before POP_ITER cleans up (iter,
+    # null_or_index). Net effect is +1 in both cases, matching dis.stack_effect.
+    "FOR_ITER": (lambda __effect, __arg, __jump: (-2, 3))
+    if PY315
+    else (lambda effect, __arg, jump: (effect, 0) if jump else (-1, 2)),
     "BUILD_INTERPOLATION": lambda effect, arg, jump: (-(2 + (arg & 1)), 1),
     **{
         # Instr(UNPACK_* , n) pops 1 and pushes n
@@ -843,6 +866,9 @@ class BaseInstr(Generic[A]):
                 "Only base opcodes are supported"
             )
 
+        if arg is UNSET and opcode in CACHE_ONLY_ARG_OPCODES:
+            arg = 0  # type: ignore
+
         self._check_arg(name, opcode, arg)
 
         self._name = name
Index: python-bytecode/src/bytecode/utils.py
===================================================================
--- python-bytecode.orig/src/bytecode/utils.py
+++ python-bytecode/src/bytecode/utils.py
@@ -6,3 +6,4 @@ PY311: Final[bool] = sys.version_info >=
 PY312: Final[bool] = sys.version_info >= (3, 12)
 PY313: Final[bool] = sys.version_info >= (3, 13)
 PY314: Final[bool] = sys.version_info >= (3, 14)
+PY315: Final[bool] = sys.version_info >= (3, 15)
Index: python-bytecode/tests/test_bytecode.py
===================================================================
--- python-bytecode.orig/tests/test_bytecode.py
+++ python-bytecode/tests/test_bytecode.py
@@ -7,8 +7,8 @@ import types
 import unittest
 
 from bytecode import Bytecode, ConcreteInstr, FreeVar, Instr, Label, SetLineno
-from bytecode.instr import BinaryOp, FormatValue, InstrLocation
-from bytecode.utils import PY310, PY311, PY312, PY313, PY314
+from bytecode.instr import BinaryOp, CommonConstant, FormatValue, InstrLocation
+from bytecode.utils import PY310, PY311, PY312, PY313, PY314, PY315
 
 from . import TestCase, get_code
 
@@ -169,6 +169,18 @@ class BytecodeTests(TestCase):
         label_else = Label()
         label_exit = Label()
         if PY314:
+
+            def _ret_none(lineno):
+                return (
+                    Instr(
+                        "LOAD_COMMON_CONSTANT",
+                        CommonConstant.CONSTANT_NONE,
+                        lineno=lineno,
+                    )
+                    if PY315
+                    else Instr("LOAD_CONST", None, lineno=lineno)
+                )
+
             self.assertInstructionListEqual(
                 bytecode,
                 [
@@ -179,12 +191,12 @@ class BytecodeTests(TestCase):
                     Instr("NOT_TAKEN", lineno=1),
                     Instr("LOAD_SMALL_INT", 1, lineno=2),
                     Instr("STORE_NAME", "x", lineno=2),
-                    Instr("LOAD_CONST", None, lineno=2),
+                    _ret_none(2),
                     Instr("RETURN_VALUE", lineno=2),
                     label_else,
                     Instr("LOAD_SMALL_INT", 2, lineno=4),
                     Instr("STORE_NAME", "x", lineno=4),
-                    Instr("LOAD_CONST", None, lineno=4),
+                    _ret_none(4),
                     Instr("RETURN_VALUE", lineno=4),
                 ],
             )
@@ -341,7 +353,11 @@ class BytecodeTests(TestCase):
             ]
             + (
                 [
-                    Instr("LOAD_CONST", None, lineno=3),
+                    Instr(
+                        "LOAD_COMMON_CONSTANT" if PY315 else "LOAD_CONST",
+                        CommonConstant.CONSTANT_NONE if PY315 else None,
+                        lineno=3,
+                    ),
                     Instr("RETURN_VALUE", lineno=3),
                 ]
                 if PY314
Index: python-bytecode/tests/test_cfg.py
===================================================================
--- python-bytecode.orig/tests/test_cfg.py
+++ python-bytecode/tests/test_cfg.py
@@ -20,7 +20,8 @@ from bytecode import (
     dump_bytecode,
 )
 from bytecode.concrete import OFFSET_AS_INSTRUCTION
-from bytecode.utils import PY311, PY312, PY313, PY314
+from bytecode.instr import CommonConstant
+from bytecode.utils import PY311, PY312, PY313, PY314, PY315
 
 from . import TestCase, disassemble as _disassemble
 
@@ -34,15 +35,20 @@ def disassemble(
         # drop LOAD_CONST+RETURN_VALUE to only keep 2 instructions,
         # to make unit tests shorter
         block = blocks[-1]
-        test = (
-            (block[-1].name == "RETURN_CONST" and block[-1].arg is None)
-            if PY312 and not PY314
-            else (
+        if PY315:
+            test = (
+                block[-2].name == "LOAD_COMMON_CONSTANT"
+                and block[-2].arg == CommonConstant.CONSTANT_NONE
+                and block[-1].name == "RETURN_VALUE"
+            )
+        elif PY312 and not PY314:
+            test = block[-1].name == "RETURN_CONST" and block[-1].arg is None
+        else:
+            test = (
                 block[-2].name == "LOAD_CONST"
                 and block[-2].arg is None
                 and block[-1].name == "RETURN_VALUE"
             )
-        )
         if not test:
             raise ValueError(
                 "unable to find implicit RETURN_VALUE <None>: %s" % block[-2:]
Index: python-bytecode/tests/test_code.py
===================================================================
--- python-bytecode.orig/tests/test_code.py
+++ python-bytecode/tests/test_code.py
@@ -84,6 +84,17 @@ class CodeTests(TestCase):
             function=True,
         )
 
+    def test_import(self):
+        # In 3.15 IMPORT_NAME gained lazy/eager flag bits packed into its arg
+        # (like LOAD_SUPER_ATTR), which a naive plain-name decode misses.
+        self.check(
+            """
+            import os
+            import os.path as osp
+            from os import path
+        """
+        )
+
 
 if __name__ == "__main__":
     unittest.main()  # pragma: no cover
Index: python-bytecode/tests/test_concrete.py
===================================================================
--- python-bytecode.orig/tests/test_concrete.py
+++ python-bytecode/tests/test_concrete.py
@@ -21,7 +21,8 @@ from bytecode import (
     SetLineno,
 )
 from bytecode.concrete import OFFSET_AS_INSTRUCTION, ExceptionTableEntry
-from bytecode.utils import PY310, PY311, PY312, PY313, PY314
+from bytecode.instr import CommonConstant
+from bytecode.utils import PY310, PY311, PY312, PY313, PY314, PY315
 
 from . import TestCase, get_code
 
@@ -250,30 +251,43 @@ class ConcreteBytecodeTests(TestCase):
     def test_attr(self):
         code_obj = get_code("x = 5")
         code = ConcreteBytecode.from_code(code_obj)
-        self.assertEqual(code.consts, [5, None])
+        self.assertEqual(code.consts, [5] if PY315 else [5, None])
         self.assertEqual(code.names, ["x"])
         self.assertEqual(code.varnames, [])
         self.assertEqual(code.freevars, [])
         self.assertInstructionListEqual(
             list(code),
-            ([ConcreteInstr("RESUME", 0, lineno=0)] if PY311 else [])
-            + [
-                ConcreteInstr("LOAD_CONST", 0, lineno=1),
-                ConcreteInstr("STORE_NAME", 0, lineno=1),
-            ]
-            + (
+            (
                 [
-                    ConcreteInstr("LOAD_SMALL_INT", 1, lineno=1),
+                    ConcreteInstr("RESUME", 0, lineno=0),
+                    ConcreteInstr("CACHE", 0, lineno=0),
+                    ConcreteInstr("LOAD_SMALL_INT", 5, lineno=1),
+                    ConcreteInstr("STORE_NAME", 0, lineno=1),
+                    ConcreteInstr("LOAD_COMMON_CONSTANT", 7, lineno=1),
                     ConcreteInstr("RETURN_VALUE", lineno=1),
                 ]
-                if PY314
+                if PY315
                 else (
-                    [ConcreteInstr("RETURN_CONST", 1, lineno=1)]
-                    if PY312
-                    else [
-                        ConcreteInstr("LOAD_CONST", 1, lineno=1),
-                        ConcreteInstr("RETURN_VALUE", lineno=1),
+                    ([ConcreteInstr("RESUME", 0, lineno=0)] if PY311 else [])
+                    + [
+                        ConcreteInstr("LOAD_CONST", 0, lineno=1),
+                        ConcreteInstr("STORE_NAME", 0, lineno=1),
                     ]
+                    + (
+                        [
+                            ConcreteInstr("LOAD_SMALL_INT", 1, lineno=1),
+                            ConcreteInstr("RETURN_VALUE", lineno=1),
+                        ]
+                        if PY314
+                        else (
+                            [ConcreteInstr("RETURN_CONST", 1, lineno=1)]
+                            if PY312
+                            else [
+                                ConcreteInstr("LOAD_CONST", 1, lineno=1),
+                                ConcreteInstr("RETURN_VALUE", lineno=1),
+                            ]
+                        )
+                    )
                 )
             ),
         )
@@ -329,7 +343,9 @@ class ConcreteBytecodeTests(TestCase):
             ]
             + (
                 [
-                    ConcreteInstr("LOAD_CONST", 1),
+                    ConcreteInstr("LOAD_COMMON_CONSTANT", CommonConstant.CONSTANT_NONE)
+                    if PY315
+                    else ConcreteInstr("LOAD_CONST", 1),
                     ConcreteInstr("RETURN_VALUE"),
                 ]
                 if PY314
@@ -466,7 +482,9 @@ class ConcreteBytecodeTests(TestCase):
             ]
             + (
                 [
-                    ConcreteInstr("LOAD_CONST", 1),
+                    ConcreteInstr("LOAD_COMMON_CONSTANT", CommonConstant.CONSTANT_NONE)
+                    if PY315
+                    else ConcreteInstr("LOAD_CONST", 1),
                     ConcreteInstr("RETURN_VALUE"),
                 ]
                 if PY314
@@ -824,6 +842,32 @@ class ConcreteFromCodeTests(TestCase):
 
         # without EXTENDED_ARG
         concrete = ConcreteBytecode.from_code(code_obj)
+        if PY315:
+            ann_code = concrete.consts[0]
+            func_code = concrete.consts[1]
+            expected_py315 = [
+                ConcreteInstr("RESUME", 0, lineno=0),
+                ConcreteInstr("CACHE", 0, lineno=0),
+                ConcreteInstr("LOAD_CONST", 0, lineno=1),
+                ConcreteInstr("MAKE_FUNCTION", lineno=1),
+                ConcreteInstr("LOAD_CONST", 1, lineno=1),
+                ConcreteInstr("MAKE_FUNCTION", lineno=1),
+                ConcreteInstr("SET_FUNCTION_ATTRIBUTE", 16, lineno=1),
+                ConcreteInstr("STORE_NAME", 0, lineno=1),
+                ConcreteInstr("LOAD_COMMON_CONSTANT", 7, lineno=1),
+                ConcreteInstr("RETURN_VALUE", lineno=1),
+            ]
+            expected_consts = [ann_code, func_code]
+            self.assertSequenceEqual(concrete.names, ["foo"])
+            self.assertSequenceEqual(concrete.consts, expected_consts)
+            self.assertInstructionListEqual(list(concrete), expected_py315)
+            concrete = ConcreteBytecode.from_code(code_obj, extended_arg=True)
+            ann_code = concrete.consts[0]
+            func_code = concrete.consts[1]
+            self.assertEqual(concrete.names, ["foo"])
+            self.assertEqual(concrete.consts, expected_consts)
+            self.assertInstructionListEqual(list(concrete), expected_py315)
+            return
         if PY314:
             ann_code = concrete.consts[0]
             func_code = concrete.consts[1]
Index: python-bytecode/tests/test_misc.py
===================================================================
--- python-bytecode.orig/tests/test_misc.py
+++ python-bytecode/tests/test_misc.py
@@ -8,7 +8,7 @@ import unittest
 import bytecode
 from bytecode import BasicBlock, Bytecode, ControlFlowGraph, Instr, Label
 from bytecode.concrete import OFFSET_AS_INSTRUCTION
-from bytecode.utils import PY311, PY312, PY313, PY314
+from bytecode.utils import PY311, PY312, PY313, PY314, PY315
 
 from . import disassemble
 
@@ -529,7 +529,33 @@ label_instr13:
         code = code.to_concrete_bytecode()
 
         # without line numbers
-        if PY314:
+        if PY315:
+            # RESUME gained a CACHE entry in 3.15, shifting all offsets by 2
+            expected = """
+  0    RESUME 0
+  2    CACHE 0
+  4    LOAD_FAST_BORROW 0
+  6    LOAD_SMALL_INT 1
+  8    COMPARE_OP 88
+ 10    CACHE 0
+ 12    POP_JUMP_IF_FALSE 3
+ 14    CACHE 0
+ 16    NOT_TAKEN
+ 18    LOAD_SMALL_INT 1
+ 20    RETURN_VALUE
+ 22    LOAD_FAST_BORROW 0
+ 24    LOAD_SMALL_INT 2
+ 26    COMPARE_OP 88
+ 28    CACHE 0
+ 30    POP_JUMP_IF_FALSE 3
+ 32    CACHE 0
+ 34    NOT_TAKEN
+ 36    LOAD_SMALL_INT 2
+ 38    RETURN_VALUE
+ 40    LOAD_SMALL_INT 3
+ 42    RETURN_VALUE
+"""
+        elif PY314:
             # COMPARE_OP use the 4 lowest bits as a cache
             expected = """
   0    RESUME 0
@@ -635,7 +661,32 @@ label_instr13:
         self.check_dump_bytecode(code, expected.lstrip("\n"))
 
         # with line numbers
-        if PY314:
+        if PY315:
+            expected = """
+L.  1   0: RESUME 0
+        2: CACHE 0
+L.  2   4: LOAD_FAST_BORROW 0
+        6: LOAD_SMALL_INT 1
+        8: COMPARE_OP 88
+       10: CACHE 0
+       12: POP_JUMP_IF_FALSE 3
+       14: CACHE 0
+       16: NOT_TAKEN
+L.  3  18: LOAD_SMALL_INT 1
+       20: RETURN_VALUE
+L.  4  22: LOAD_FAST_BORROW 0
+       24: LOAD_SMALL_INT 2
+       26: COMPARE_OP 88
+       28: CACHE 0
+       30: POP_JUMP_IF_FALSE 3
+       32: CACHE 0
+       34: NOT_TAKEN
+L.  5  36: LOAD_SMALL_INT 2
+       38: RETURN_VALUE
+L.  6  40: LOAD_SMALL_INT 3
+       42: RETURN_VALUE
+"""
+        elif PY314:
             expected = """
 L.  1   0: RESUME 0
 L.  2   2: LOAD_FAST_BORROW 0

Reply via email to