This is an automated email from the ASF dual-hosted git repository.
jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 84b62218f1e Update Trivial Inference for Python 3.15 (#40578)
84b62218f1e is described below
commit 84b62218f1e7adecd7791c2dabe27fef2a8d277f
Author: Jack McCluskey <[email protected]>
AuthorDate: Thu Oct 15 09:55:00 2026 -0400
Update Trivial Inference for Python 3.15 (#40578)
---
sdks/python/apache_beam/typehints/opcodes.py | 26 +++++++++++++++++++++-
.../apache_beam/typehints/trivial_inference.py | 23 ++++++++++++++-----
2 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/sdks/python/apache_beam/typehints/opcodes.py
b/sdks/python/apache_beam/typehints/opcodes.py
index 53eabdadc4a..c5dda817d0d 100644
--- a/sdks/python/apache_beam/typehints/opcodes.py
+++ b/sdks/python/apache_beam/typehints/opcodes.py
@@ -67,8 +67,13 @@ else:
if sys.version_info >= (3, 14):
_NB_SUBSCR_OPCODE = [op[0] for op in opcode._nb_ops].index('NB_SUBSCR')
+ # Table of values indexed by the arg of LOAD_COMMON_CONSTANT. In 3.15 this
+ # was expanded to include common literals such as None, '', True, False,
+ # and -1, which are no longer loaded via LOAD_CONST.
+ _COMMON_CONSTANTS = tuple(getattr(opcode, '_common_constants', ()))
else:
_NB_SUBSCR_OPCODE = -1
+ _COMMON_CONSTANTS = ()
def pop_one(state, unused_arg):
@@ -107,7 +112,11 @@ def end_for(state, unused_arg):
def end_send(state, unused_arg):
- del state.stack[-2]
+ if sys.version_info >= (3, 15):
+ # The receiver is a two-slot (iter, index_or_null) pair in 3.15.
+ del state.stack[-3:-1]
+ else:
+ del state.stack[-2]
def copy(state, arg):
@@ -151,6 +160,14 @@ def unary_convert(state, unused_arg):
def get_iter(state, unused_arg):
state.stack.append(Iterable[element_type(state.stack.pop())])
+ if sys.version_info >= (3, 15):
+ # In 3.15 the iteration state occupies two stack slots: the iterator (or
+ # the iterable itself for "virtual" iteration over sequences) and either
+ # NULL or a tagged integer index. FOR_ITER, POP_ITER, END_SEND, etc. all
+ # operate on this pair. The second slot is irrelevant for type inference
+ # but must be tracked so stack-relative args (e.g. LIST_APPEND, SWAP)
+ # stay correct.
+ state.stack.append(Any)
_NUMERIC_PROMOTION_LADDER = [bool, int, float, complex]
@@ -506,6 +523,13 @@ def load_small_int(state, arg):
state.stack.append(Const(arg))
+def load_common_constant(state, arg):
+ if arg < len(_COMMON_CONSTANTS):
+ state.stack.append(Const(_COMMON_CONSTANTS[arg]))
+ else:
+ state.stack.append(Any)
+
+
store_map = pop_two
diff --git a/sdks/python/apache_beam/typehints/trivial_inference.py
b/sdks/python/apache_beam/typehints/trivial_inference.py
index 69edfc30928..e34dbb98fbb 100644
--- a/sdks/python/apache_beam/typehints/trivial_inference.py
+++ b/sdks/python/apache_beam/typehints/trivial_inference.py
@@ -578,7 +578,11 @@ def infer_return_type_func(f, input_types, debug=False,
depth=0):
else:
# Handle comprehensions always having an arg of 0 for CALL
# See https://github.com/python/cpython/issues/102403 for context.
- if (pop_count == 1 and last_real_opname == 'GET_ITER' and
+ # In 3.15 the GET_ITER for a generator expression's iterable moved
+ # into the generator's own code object, so the CALL is no longer
+ # preceded by a GET_ITER in the calling frame.
+ if (pop_count == 1 and
+ (last_real_opname == 'GET_ITER' or sys.version_info >= (3, 15)) and
len(state.stack) > 1 and isinstance(state.stack[-2], Const) and
getattr(state.stack[-2].value, '__name__', None)
in ('<listcomp>', '<dictcomp>', '<setcomp>', '<genexpr>')):
@@ -692,11 +696,20 @@ def infer_return_type_func(f, input_types, debug=False,
depth=0):
# so jump 4 more bytes.
jmp += 4
jmp_state = state.copy()
- jmp_state.stack.pop()
- state.stack.append(element_type(state.stack[-1]))
+ if sys.version_info >= (3, 15):
+ # In 3.15 the iterator occupies two stack slots (iter, index_or_null)
+ # and both are popped when the iterator is exhausted.
+ jmp_state.stack[-2:] = []
+ state.stack.append(element_type(state.stack[-2]))
+ else:
+ jmp_state.stack.pop()
+ state.stack.append(element_type(state.stack[-1]))
elif opname == 'POP_ITER':
- # Introduced in 3.14.
- state.stack.pop()
+ # Introduced in 3.14. Pops (iter, index_or_null) in 3.15.
+ if sys.version_info >= (3, 15):
+ state.stack[-2:] = []
+ else:
+ state.stack.pop()
elif opname == 'COPY_FREE_VARS':
# Helps with calling closures, but since we aren't executing
# them we can treat this as a no-op