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 fdd25a4eeb6 Update trivial inference for Python 3.14 (#37248)
fdd25a4eeb6 is described below

commit fdd25a4eeb64466af0c00d3edc76fd8a74237fc1
Author: Jack McCluskey <[email protected]>
AuthorDate: Tue Jan 13 13:23:33 2026 -0500

    Update trivial inference for Python 3.14 (#37248)
    
    * Update trivial inference for Python 3.14
    
    * correct comment
    
    * Address review coments
    
    * avoid none case being incorrect
    
    * fix docstring
---
 .../typehints/native_type_compatibility.py         |  2 +-
 .../typehints/native_type_compatibility_test.py    |  2 +-
 sdks/python/apache_beam/typehints/opcodes.py       | 24 ++++++++++++++++++-
 .../apache_beam/typehints/trivial_inference.py     | 27 +++++++++++++++++++++-
 4 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/sdks/python/apache_beam/typehints/native_type_compatibility.py 
b/sdks/python/apache_beam/typehints/native_type_compatibility.py
index 7cdfa0721ff..345c04706d6 100644
--- a/sdks/python/apache_beam/typehints/native_type_compatibility.py
+++ b/sdks/python/apache_beam/typehints/native_type_compatibility.py
@@ -95,7 +95,7 @@ def _get_args(typ):
     A tuple of args.
   """
   try:
-    if typ.__args__ is None:
+    if typ.__args__ is None or not isinstance(typ.__args__, tuple):
       return ()
     return typ.__args__
   except AttributeError:
diff --git 
a/sdks/python/apache_beam/typehints/native_type_compatibility_test.py 
b/sdks/python/apache_beam/typehints/native_type_compatibility_test.py
index 0e933b0d492..e9ce732d2e9 100644
--- a/sdks/python/apache_beam/typehints/native_type_compatibility_test.py
+++ b/sdks/python/apache_beam/typehints/native_type_compatibility_test.py
@@ -337,7 +337,7 @@ class NativeTypeCompatibilityTest(unittest.TestCase):
     self.assertEqual(typehints.Any, convert_to_beam_type('int'))
     self.assertEqual(typehints.Any, convert_to_beam_type('typing.List[int]'))
     self.assertEqual(
-        typehints.List[typehints.Any], 
convert_to_beam_type(typing.List['int']))
+        typehints.List[typehints.Any], convert_to_beam_type(list['int']))
 
   def test_convert_nested_to_beam_type(self):
     self.assertEqual(typehints.List[typing.Any], typehints.List[typehints.Any])
diff --git a/sdks/python/apache_beam/typehints/opcodes.py 
b/sdks/python/apache_beam/typehints/opcodes.py
index d94221c7b86..8e5d7b1e40c 100644
--- a/sdks/python/apache_beam/typehints/opcodes.py
+++ b/sdks/python/apache_beam/typehints/opcodes.py
@@ -63,6 +63,11 @@ if sys.version_info >= (3, 11):
 else:
   _div_binop_args = frozenset()
 
+if sys.version_info >= (3, 14):
+  _NB_SUBSCR_OPCODE = [op[0] for op in opcode._nb_ops].index('NB_SUBSCR')
+else:
+  _NB_SUBSCR_OPCODE = -1
+
 
 def pop_one(state, unused_arg):
   del state.stack[-1:]
@@ -151,6 +156,9 @@ _NUMERIC_PROMOTION_LADDER = [bool, int, float, complex]
 
 def symmetric_binary_op(state, arg, is_true_div=None):
   # TODO(robertwb): This may not be entirely correct...
+  # BINARY_SUBSCR was rolled into BINARY_OP in 3.14.
+  if arg == _NB_SUBSCR_OPCODE:
+    return binary_subscr(state, arg)
   b, a = Const.unwrap(state.stack.pop()), Const.unwrap(state.stack.pop())
   if a == b:
     if a is int and b is int and (arg in _div_binop_args or is_true_div):
@@ -206,7 +214,10 @@ def binary_subscr(state, unused_arg):
       out = base._constraint_for_index(index.value)
     except IndexError:
       out = element_type(base)
-  elif index == slice and isinstance(base, typehints.IndexableTypeConstraint):
+  elif (index == slice or getattr(index, 'type', None) == slice) and 
isinstance(
+      base, typehints.IndexableTypeConstraint):
+    # The slice is treated as a const in 3.14, using this instead of
+    # BINARY_SLICE
     out = base
   else:
     out = element_type(base)
@@ -483,6 +494,10 @@ def load_global(state, arg):
   state.stack.append(state.get_global(arg))
 
 
+def load_small_int(state, arg):
+  state.stack.append(Const(arg))
+
+
 store_map = pop_two
 
 
@@ -490,6 +505,9 @@ def load_fast(state, arg):
   state.stack.append(state.vars[arg])
 
 
+load_fast_borrow = load_fast
+
+
 def load_fast_load_fast(state, arg):
   arg1 = arg >> 4
   arg2 = arg & 15
@@ -497,6 +515,8 @@ def load_fast_load_fast(state, arg):
   state.stack.append(state.vars[arg2])
 
 
+load_fast_borrow_load_fast_borrow = load_fast_load_fast
+
 load_fast_check = load_fast
 
 
@@ -605,6 +625,8 @@ def set_function_attribute(state, arg):
                     for t in state.stack[attr].tuple_types)
   new_func = types.FunctionType(
       func.code, func.globals, name=func.name, closure=closure)
+  if arg & 0x10:
+    new_func.__annotate__ = attr
   state.stack.append(Const(new_func))
 
 
diff --git a/sdks/python/apache_beam/typehints/trivial_inference.py 
b/sdks/python/apache_beam/typehints/trivial_inference.py
index 8593e2729ed..68e126a8939 100644
--- a/sdks/python/apache_beam/typehints/trivial_inference.py
+++ b/sdks/python/apache_beam/typehints/trivial_inference.py
@@ -396,6 +396,11 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
 
   jump_multiplier = 2
 
+  # Python 3.14+ push nulls are used to signal kwargs for CALL_FUNCTION_EX
+  # so there must be a little extra bookkeeping even if we don't care about
+  # the nulls themselves.
+  last_op_push_null = 0
+
   last_pc = -1
   last_real_opname = opname = None
   while pc < end:  # pylint: disable=too-many-nested-blocks
@@ -441,7 +446,8 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
         elif op in dis.haslocal:
           # Args to double-fast opcodes are bit manipulated, correct the arg
           # for printing + avoid the out-of-index
-          if dis.opname[op] == 'LOAD_FAST_LOAD_FAST':
+          if dis.opname[op] == 'LOAD_FAST_LOAD_FAST' or dis.opname[
+              op] == "LOAD_FAST_BORROW_LOAD_FAST_BORROW":
             print(
                 '(' + co.co_varnames[arg >> 4] + ', ' +
                 co.co_varnames[arg & 15] + ')',
@@ -450,6 +456,8 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
             print('(' + co.co_varnames[arg & 15] + ')', end=' ')
           elif dis.opname[op] == 'STORE_FAST_STORE_FAST':
             pass
+          elif dis.opname[op] == 'LOAD_DEREF':
+            pass
           else:
             print('(' + co.co_varnames[arg] + ')', end=' ')
         elif op in dis.hascompare:
@@ -512,6 +520,12 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
         # stack[-has_kwargs]: Map of keyword args.
         # stack[-1 - has_kwargs]: Iterable of positional args.
         # stack[-2 - has_kwargs]: Function to call.
+        if arg is None:
+          # CALL_FUNCTION_EX does not take an arg in 3.14, instead the
+          # signaling for kwargs is done via a PUSH_NULL instruction
+          # right before CALL_FUNCTION_EX. A PUSH_NULL indicates that
+          # there are no kwargs.
+          arg = ~last_op_push_null
         has_kwargs: int = arg & 1
         pop_count = has_kwargs + 2
         if has_kwargs:
@@ -680,6 +694,9 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
       jmp_state = state.copy()
       jmp_state.stack.pop()
       state.stack.append(element_type(state.stack[-1]))
+    elif opname == 'POP_ITER':
+      # Introduced in 3.14.
+      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
@@ -694,6 +711,10 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
       # We're treating this as a no-op to avoid having to check
       # for extra None values on the stack when we extract return
       # values
+      last_op_push_null = 1
+      pass
+    elif opname == 'NOT_TAKEN':
+      # NOT_TAKEN is a no-op introduced in 3.14.
       pass
     elif opname == 'PRECALL':
       # PRECALL is a no-op.
@@ -727,6 +748,10 @@ def infer_return_type_func(f, input_types, debug=False, 
depth=0):
     else:
       raise TypeInferenceError('unable to handle %s' % opname)
 
+    # Clear check for previous push_null.
+    if opname != 'PUSH_NULL' and last_op_push_null == 1:
+      last_op_push_null = 0
+
     if jmp is not None:
       # TODO(robertwb): Is this guaranteed to converge?
       new_state = states[jmp] | jmp_state

Reply via email to