Skip to content

Commit 130e992

Browse files
committed
feat: add support for CPython 3.15
1 parent 66584dc commit 130e992

7 files changed

Lines changed: 130 additions & 23 deletions

File tree

.github/workflows/cis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ jobs:
5050
toxenv: py313
5151
- python-version: "3.14"
5252
toxenv: py314
53+
- python-version: "3.15-dev"
54+
toxenv: py315
5355
steps:
5456
- uses: actions/checkout@v6
5557
- name: Get history and tags for SCM versioning to work

src/bytecode/instr.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing_extensions import TypeGuard # type: ignore
1717

1818
import bytecode as _bytecode
19-
from bytecode.utils import PY312, PY313, PY314
19+
from bytecode.utils import PY312, PY313, PY314, PY315
2020

2121
# --- Instruction argument tools and
2222

@@ -53,6 +53,11 @@
5353
# Small integer related opcode
5454
SMALL_INT_OPS: Final[set[int]] = {_opcode.opmap["LOAD_SMALL_INT"]} if PY314 else set()
5555

56+
# Opcodes that gained a cache-only argument in 3.15 (arg is always 0 and not user-visible)
57+
CACHE_ONLY_ARG_OPCODES: Final[set[int]] = (
58+
{_opcode.opmap["GET_ITER"]} if PY315 else set()
59+
)
60+
5661
# Special method loading related opcodes
5762
SPECIAL_OPS: Final[set[int]] = {_opcode.opmap["LOAD_SPECIAL"]} if PY314 else set()
5863

@@ -419,8 +424,8 @@ def opcode_has_argument(opcode: int) -> bool:
419424
"DUP_TOP": (-1, 2),
420425
"DUP_TOP_TWO": (-2, 4),
421426
"GET_LEN": (-1, 2),
422-
"GET_ITER": (-1, 1),
423-
"GET_YIELD_FROM_ITER": (-1, 1),
427+
"GET_ITER": (-1, 2) if PY315 else (-1, 1),
428+
"GET_YIELD_FROM_ITER": (-1, 1), # removed in 3.15, filtered by if k in _opcode.opmap
424429
"GET_AWAITABLE": (-1, 1),
425430
"GET_AITER": (-1, 1),
426431
"GET_ANEXT": (-1, 2),
@@ -503,7 +508,14 @@ def opcode_has_argument(opcode: int) -> bool:
503508
"MAP_ADD": lambda effect, arg, jump: (-arg, arg - 2),
504509
"FORMAT_VALUE": lambda effect, arg, jump: (effect - 1, 1),
505510
# FOR_ITER needs TOS to be an iterator, hence a prerequisite of 1 on the stack
506-
"FOR_ITER": lambda effect, arg, jump: (effect, 0) if jump else (-1, 2),
511+
# In 3.15, GET_ITER pushes (iter, null_or_index); FOR_ITER always pushes the
512+
# next value (+1). When exhausted it jumps to END_FOR (which pops it) then
513+
# POP_ITER cleans up (iter, null_or_index). Matches dis.stack_effect = 1 always.
514+
"FOR_ITER": (
515+
lambda __effect, __arg, __jump: (0, 1)
516+
) if PY315 else (
517+
lambda effect, __arg, jump: (effect, 0) if jump else (-1, 2)
518+
),
507519
"BUILD_INTERPOLATION": lambda effect, arg, jump: (-(2 + (arg & 1)), 1),
508520
**{
509521
# Instr(UNPACK_* , n) pops 1 and pushes n
@@ -875,6 +887,9 @@ def _set(self, name: str, arg: A) -> None:
875887
"Only base opcodes are supported"
876888
)
877889

890+
if arg is UNSET and opcode in CACHE_ONLY_ARG_OPCODES:
891+
arg = 0 # type: ignore
892+
878893
self._check_arg(name, opcode, arg)
879894

880895
self._name = name

src/bytecode/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
PY312: Final[bool] = sys.version_info >= (3, 12)
55
PY313: Final[bool] = sys.version_info >= (3, 13)
66
PY314: Final[bool] = sys.version_info >= (3, 14)
7+
PY315: Final[bool] = sys.version_info >= (3, 15)

tests/test_cfg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
SetLineno,
2020
dump_bytecode,
2121
)
22-
from bytecode.utils import PY312, PY313, PY314
22+
from bytecode.utils import PY312, PY313, PY314, PY315
2323

2424
from . import TestCase, disassemble as _disassemble
2525

tests/test_concrete.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
SetLineno,
2222
)
2323
from bytecode.concrete import ExceptionTableEntry
24-
from bytecode.utils import PY312, PY313, PY314
24+
from bytecode.utils import PY312, PY313, PY314, PY315
2525

2626
from . import TestCase, get_code
2727

@@ -250,24 +250,37 @@ def test_attr(self):
250250
self.assertEqual(code.freevars, [])
251251
self.assertInstructionListEqual(
252252
list(code),
253-
([ConcreteInstr("RESUME", 0, lineno=0)])
254-
+ [
255-
ConcreteInstr("LOAD_CONST", 0, lineno=1),
256-
ConcreteInstr("STORE_NAME", 0, lineno=1),
257-
]
258-
+ (
253+
(
259254
[
260-
ConcreteInstr("LOAD_SMALL_INT", 1, lineno=1),
255+
ConcreteInstr("RESUME", 0, lineno=0),
256+
ConcreteInstr("CACHE", 0, lineno=0),
257+
ConcreteInstr("LOAD_SMALL_INT", 5, lineno=1),
258+
ConcreteInstr("STORE_NAME", 0, lineno=1),
259+
ConcreteInstr("LOAD_CONST", 1, lineno=1),
261260
ConcreteInstr("RETURN_VALUE", lineno=1),
262261
]
263-
if PY314
262+
if PY315
264263
else (
265-
[ConcreteInstr("RETURN_CONST", 1, lineno=1)]
266-
if PY312
267-
else [
268-
ConcreteInstr("LOAD_CONST", 1, lineno=1),
269-
ConcreteInstr("RETURN_VALUE", lineno=1),
264+
[ConcreteInstr("RESUME", 0, lineno=0)]
265+
+ [
266+
ConcreteInstr("LOAD_CONST", 0, lineno=1),
267+
ConcreteInstr("STORE_NAME", 0, lineno=1),
270268
]
269+
+ (
270+
[
271+
ConcreteInstr("LOAD_SMALL_INT", 1, lineno=1),
272+
ConcreteInstr("RETURN_VALUE", lineno=1),
273+
]
274+
if PY314
275+
else (
276+
[ConcreteInstr("RETURN_CONST", 1, lineno=1)]
277+
if PY312
278+
else [
279+
ConcreteInstr("LOAD_CONST", 1, lineno=1),
280+
ConcreteInstr("RETURN_VALUE", lineno=1),
281+
]
282+
)
283+
)
271284
)
272285
),
273286
)
@@ -739,6 +752,31 @@ def foo(x: int, y: int):
739752

740753
# without EXTENDED_ARG
741754
concrete = ConcreteBytecode.from_code(code_obj)
755+
if PY315:
756+
ann_code = concrete.consts[0]
757+
func_code = concrete.consts[1]
758+
expected_py315 = [
759+
ConcreteInstr("RESUME", 0, lineno=0),
760+
ConcreteInstr("CACHE", 0, lineno=0),
761+
ConcreteInstr("LOAD_CONST", 0, lineno=1),
762+
ConcreteInstr("MAKE_FUNCTION", lineno=1),
763+
ConcreteInstr("LOAD_CONST", 1, lineno=1),
764+
ConcreteInstr("MAKE_FUNCTION", lineno=1),
765+
ConcreteInstr("SET_FUNCTION_ATTRIBUTE", 16, lineno=1),
766+
ConcreteInstr("STORE_NAME", 0, lineno=1),
767+
ConcreteInstr("LOAD_CONST", 2, lineno=1),
768+
ConcreteInstr("RETURN_VALUE", lineno=1),
769+
]
770+
self.assertSequenceEqual(concrete.names, ["foo"])
771+
self.assertSequenceEqual(concrete.consts, [ann_code, func_code, None])
772+
self.assertInstructionListEqual(list(concrete), expected_py315)
773+
concrete = ConcreteBytecode.from_code(code_obj, extended_arg=True)
774+
ann_code = concrete.consts[0]
775+
func_code = concrete.consts[1]
776+
self.assertEqual(concrete.names, ["foo"])
777+
self.assertEqual(concrete.consts, [ann_code, func_code, None])
778+
self.assertInstructionListEqual(list(concrete), expected_py315)
779+
return
742780
if PY314:
743781
ann_code = concrete.consts[0]
744782
func_code = concrete.consts[1]

tests/test_misc.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import bytecode
99
from bytecode import BasicBlock, Bytecode, ControlFlowGraph, Instr, Label
10-
from bytecode.utils import PY312, PY313, PY314
10+
from bytecode.utils import PY312, PY313, PY314, PY315
1111

1212
from . import disassemble
1313

@@ -422,7 +422,33 @@ def func(test):
422422
code = code.to_concrete_bytecode()
423423

424424
# without line numbers
425-
if PY314:
425+
if PY315:
426+
# RESUME gained a CACHE entry in 3.15, shifting all offsets by 2
427+
expected = """
428+
0 RESUME 0
429+
2 CACHE 0
430+
4 LOAD_FAST_BORROW 0
431+
6 LOAD_SMALL_INT 1
432+
8 COMPARE_OP 88
433+
10 CACHE 0
434+
12 POP_JUMP_IF_FALSE 3
435+
14 CACHE 0
436+
16 NOT_TAKEN
437+
18 LOAD_SMALL_INT 1
438+
20 RETURN_VALUE
439+
22 LOAD_FAST_BORROW 0
440+
24 LOAD_SMALL_INT 2
441+
26 COMPARE_OP 88
442+
28 CACHE 0
443+
30 POP_JUMP_IF_FALSE 3
444+
32 CACHE 0
445+
34 NOT_TAKEN
446+
36 LOAD_SMALL_INT 2
447+
38 RETURN_VALUE
448+
40 LOAD_SMALL_INT 3
449+
42 RETURN_VALUE
450+
"""
451+
elif PY314:
426452
# COMPARE_OP use the 4 lowest bits as a cache
427453
expected = """
428454
0 RESUME 0
@@ -511,7 +537,32 @@ def func(test):
511537
self.check_dump_bytecode(code, expected.lstrip("\n"))
512538

513539
# with line numbers
514-
if PY314:
540+
if PY315:
541+
expected = """
542+
L. 1 0: RESUME 0
543+
2: CACHE 0
544+
L. 2 4: LOAD_FAST_BORROW 0
545+
6: LOAD_SMALL_INT 1
546+
8: COMPARE_OP 88
547+
10: CACHE 0
548+
12: POP_JUMP_IF_FALSE 3
549+
14: CACHE 0
550+
16: NOT_TAKEN
551+
L. 3 18: LOAD_SMALL_INT 1
552+
20: RETURN_VALUE
553+
L. 4 22: LOAD_FAST_BORROW 0
554+
24: LOAD_SMALL_INT 2
555+
26: COMPARE_OP 88
556+
28: CACHE 0
557+
30: POP_JUMP_IF_FALSE 3
558+
32: CACHE 0
559+
34: NOT_TAKEN
560+
L. 5 36: LOAD_SMALL_INT 2
561+
38: RETURN_VALUE
562+
L. 6 40: LOAD_SMALL_INT 3
563+
42: RETURN_VALUE
564+
"""
565+
elif PY314:
515566
expected = """
516567
L. 1 0: RESUME 0
517568
L. 2 2: LOAD_FAST_BORROW 0

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py3, py38, py39, py310, py311, py312, py313, py314, fmt, docs
2+
envlist = py3, py38, py39, py310, py311, py312, py313, py314, py315, fmt, docs
33
isolated_build = true
44

55
[testenv]

0 commit comments

Comments
 (0)