diff --git a/ddtrace/debugging/_expressions.py b/ddtrace/debugging/_expressions.py index 71dc13654b6..1efa4238aa0 100644 --- a/ddtrace/debugging/_expressions.py +++ b/ddtrace/debugging/_expressions.py @@ -39,6 +39,7 @@ from typing import Tuple from typing import Union +from bytecode import BinaryOp from bytecode import Bytecode from bytecode import Compare from bytecode import Instr @@ -301,7 +302,12 @@ def _compile_arg_operation(self, ast: DDASTType) -> Optional[List[Instr]]: raise ValueError("Invalid argument: %r" % a) if cb is None: raise ValueError("Invalid argument: %r" % b) - return cv + ca + cb + [Instr("BUILD_SLICE", 2), Instr("BINARY_SUBSCR")] + + if PY >= (3, 14): + subscr_instruction = Instr("BINARY_OP", BinaryOp.SUBSCR) + else: + subscr_instruction = Instr("BINARY_SUBSCR") + return cv + ca + cb + [Instr("BUILD_SLICE", 2), subscr_instruction] if _type == "filter": a, b = args diff --git a/ddtrace/internal/assembly.py b/ddtrace/internal/assembly.py index c1740192540..0d099ed9af2 100644 --- a/ddtrace/internal/assembly.py +++ b/ddtrace/internal/assembly.py @@ -46,7 +46,7 @@ def transform_instruction(opcode: str, arg: t.Any) -> t.Tuple[str, t.Any]: opcode = "LOAD_ATTR" arg = (True, arg) elif opcode.upper() == "LOAD_ATTR" and not isinstance(arg, tuple): - arg = (False, arg) + arg = (sys.version_info >= (3, 14), arg) return opcode, arg @@ -157,6 +157,11 @@ def parse_try_end(self, line: str) -> t.Optional[bc.TryEnd]: def parse_opcode(self, text: str) -> str: opcode = text.upper() + + # `dis` doesn't include `LOAD_METHOD` in 3.14.0rc1 + if sys.version_info >= (3, 14) and opcode == "LOAD_METHOD": + return opcode + if opcode not in dis.opmap: raise ValueError("unknown opcode %s" % opcode) diff --git a/ddtrace/internal/bytecode_injection/__init__.py b/ddtrace/internal/bytecode_injection/__init__.py index b31e52e3140..a151fbea654 100644 --- a/ddtrace/internal/bytecode_injection/__init__.py +++ b/ddtrace/internal/bytecode_injection/__init__.py @@ -30,8 +30,8 @@ class InvalidLine(Exception): # the stack to the state prior to the call. INJECTION_ASSEMBLY = Assembly() -if PY >= (3, 14): - raise NotImplementedError("Python >= 3.14 is not supported yet") +if PY >= (3, 15): + raise NotImplementedError("Python >= 3.15 is not supported yet") elif PY >= (3, 13): INJECTION_ASSEMBLY.parse( r""" diff --git a/ddtrace/internal/symbol_db/symbols.py b/ddtrace/internal/symbol_db/symbols.py index 803ce59cd11..050e9655902 100644 --- a/ddtrace/internal/symbol_db/symbols.py +++ b/ddtrace/internal/symbol_db/symbols.py @@ -96,7 +96,8 @@ def get_fields(cls: type) -> t.Set[str]: return { code.co_names[b.arg] for a, b in zip(*(islice(t, i, None) for i, t in enumerate(tee(dis.get_instructions(code), 2)))) - if a.opname == "LOAD_FAST" and a.arg == 0 and b.opname == "STORE_ATTR" + # Python 3.14 changed this to LOAD_FAST_BORROW + if a.opname.startswith("LOAD_FAST") and a.arg & 15 == 0 and b.opname == "STORE_ATTR" } except AttributeError: return set() diff --git a/ddtrace/internal/wrapping/asyncs.py b/ddtrace/internal/wrapping/asyncs.py index 66341657ca4..e6288eb9175 100644 --- a/ddtrace/internal/wrapping/asyncs.py +++ b/ddtrace/internal/wrapping/asyncs.py @@ -34,7 +34,133 @@ ASYNC_GEN_ASSEMBLY = Assembly() ASYNC_HEAD_ASSEMBLY = None -if PY >= (3, 12): +if PY >= (3, 14): + ASYNC_HEAD_ASSEMBLY = Assembly() + ASYNC_HEAD_ASSEMBLY.parse( + r""" + return_generator + pop_top + """ + ) + + COROUTINE_ASSEMBLY.parse( + r""" + get_awaitable 0 + load_const None + + presend: + send @send + yield_value 2 + resume 3 + jump_backward_no_interrupt @presend + send: + end_send + """ + ) + + ASYNC_GEN_ASSEMBLY.parse( + r""" + try @stopiter + copy 1 + store_fast $__ddgen + load_attr (False, 'asend') + store_fast $__ddgensend + load_fast $__ddgen + load_attr (True, '__anext__') + call 0 + + loop: + get_awaitable 0 + load_const None + presend0: + send @send0 + tried + + try @genexit lasti + yield_value 3 + resume 3 + jump_backward_no_interrupt @loop + send0: + end_send + + yield: + call_intrinsic_1 asm.Intrinsic1Op.INTRINSIC_ASYNC_GEN_WRAP + yield_value 3 + resume 1 + push_null + swap 2 + load_fast $__ddgensend + swap 2 + call 1 + jump_backward @loop + tried + + genexit: + try @stopiter + push_exc_info + load_const GeneratorExit + check_exc_match + pop_jump_if_false @exc + pop_top + load_fast $__ddgen + load_attr (True, 'aclose') + call 0 + get_awaitable 0 + load_const None + + presend1: + send @send1 + yield_value 4 + resume 3 + jump_backward_no_interrupt @presend1 + send1: + end_send + pop_top + pop_except + load_const None + return_value + + exc: + pop_top + push_null + load_fast $__ddgen + load_attr (False, 'athrow') + push_null + load_const sys.exc_info + call 0 + call_function_ex + get_awaitable 0 + load_const None + + presend2: + send @send2 + yield_value 4 + resume 3 + jump_backward_no_interrupt @presend2 + send2: + end_send + swap 2 + pop_except + jump_backward @yield + tried + + stopiter: + push_exc_info + load_const StopAsyncIteration + check_exc_match + pop_jump_if_false @propagate + pop_top + pop_except + load_const None + return_value + + propagate: + reraise 0 + """ + ) + + +elif PY >= (3, 12): ASYNC_HEAD_ASSEMBLY = Assembly() ASYNC_HEAD_ASSEMBLY.parse( r""" diff --git a/ddtrace/internal/wrapping/context.py b/ddtrace/internal/wrapping/context.py index 87ce3430855..2e5be4b1013 100644 --- a/ddtrace/internal/wrapping/context.py +++ b/ddtrace/internal/wrapping/context.py @@ -67,8 +67,8 @@ CONTEXT_RETURN = Assembly() CONTEXT_FOOT = Assembly() -if sys.version_info >= (3, 14): - raise NotImplementedError("Python >= 3.14 is not supported yet") +if sys.version_info >= (3, 15): + raise NotImplementedError("Python >= 3.15 is not supported yet") elif sys.version_info >= (3, 13): CONTEXT_HEAD.parse( r""" diff --git a/ddtrace/internal/wrapping/generators.py b/ddtrace/internal/wrapping/generators.py index 91cbed49962..37b762b64e5 100644 --- a/ddtrace/internal/wrapping/generators.py +++ b/ddtrace/internal/wrapping/generators.py @@ -30,7 +30,85 @@ GENERATOR_ASSEMBLY = Assembly() GENERATOR_HEAD_ASSEMBLY = None -if PY >= (3, 12): +if PY >= (3, 14): + GENERATOR_HEAD_ASSEMBLY = Assembly() + GENERATOR_HEAD_ASSEMBLY.parse( + r""" + return_generator + pop_top + """ + ) + + GENERATOR_ASSEMBLY.parse( + r""" + try @stopiter + copy 1 + store_fast $__ddgen + load_attr $send + store_fast $__ddgensend + push_null + load_const next + load_fast $__ddgen + + loop: + call 1 + tried + + yield: + try @genexit lasti + yield_value 3 + resume 1 + push_null + swap 2 + load_fast $__ddgensend + swap 2 + jump_backward @loop + tried + + genexit: + try @stopiter + push_exc_info + load_const GeneratorExit + check_exc_match + pop_jump_if_false @exc + pop_top + load_fast $__ddgen + load_method $close + call 0 + swap 2 + pop_except + return_value + + exc: + pop_top + push_null + load_fast $__ddgen + load_attr $throw + push_null + load_const sys.exc_info + call 0 + call_function_ex + swap 2 + pop_except + jump_backward @yield + tried + + stopiter: + push_exc_info + load_const StopIteration + check_exc_match + pop_jump_if_false @propagate + pop_top + pop_except + load_const None + return_value + + propagate: + reraise 0 + """ + ) + +elif PY >= (3, 12): GENERATOR_HEAD_ASSEMBLY = Assembly() GENERATOR_HEAD_ASSEMBLY.parse( r"""