From af6d8b683002a3d22a4e92c5dd72e5d9b744b762 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 18 Nov 2025 11:02:22 +0000 Subject: [PATCH] chore: fix wrapping for 3.13+ We fix the bytecode wrapping to properly support 3.13 and later versions of CPython. --- ddtrace/internal/assembly.py | 2 +- ddtrace/internal/wrapping/asyncs.py | 144 ++++++++++++++++++++++-- ddtrace/internal/wrapping/generators.py | 92 ++++++++++++++- tests/internal/test_wrapping.py | 13 +-- 4 files changed, 224 insertions(+), 27 deletions(-) diff --git a/ddtrace/internal/assembly.py b/ddtrace/internal/assembly.py index 0d099ed9af2..47ec3c56b55 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 = (sys.version_info >= (3, 14), arg) + arg = (False, arg) return opcode, arg diff --git a/ddtrace/internal/wrapping/asyncs.py b/ddtrace/internal/wrapping/asyncs.py index e6288eb9175..6181acf1a2b 100644 --- a/ddtrace/internal/wrapping/asyncs.py +++ b/ddtrace/internal/wrapping/asyncs.py @@ -34,7 +34,10 @@ ASYNC_GEN_ASSEMBLY = Assembly() ASYNC_HEAD_ASSEMBLY = None -if PY >= (3, 14): +if PY >= (3, 15): + raise NotImplementedError("This version of CPython is not supported yet") + +elif PY >= (3, 14): ASYNC_HEAD_ASSEMBLY = Assembly() ASYNC_HEAD_ASSEMBLY.parse( r""" @@ -50,7 +53,7 @@ presend: send @send - yield_value 2 + yield_value 0 resume 3 jump_backward_no_interrupt @presend send: @@ -77,7 +80,7 @@ tried try @genexit lasti - yield_value 3 + yield_value 0 resume 3 jump_backward_no_interrupt @loop send0: @@ -85,12 +88,11 @@ yield: call_intrinsic_1 asm.Intrinsic1Op.INTRINSIC_ASYNC_GEN_WRAP - yield_value 3 + yield_value 0 resume 1 push_null - swap 2 load_fast $__ddgensend - swap 2 + swap 3 call 1 jump_backward @loop tried @@ -110,7 +112,7 @@ presend1: send @send1 - yield_value 4 + yield_value 0 resume 3 jump_backward_no_interrupt @presend1 send1: @@ -122,19 +124,20 @@ exc: pop_top - push_null load_fast $__ddgen load_attr (False, 'athrow') push_null load_const sys.exc_info + push_null call 0 + push_null call_function_ex get_awaitable 0 load_const None presend2: send @send2 - yield_value 4 + yield_value 0 resume 3 jump_backward_no_interrupt @presend2 send2: @@ -159,6 +162,129 @@ """ ) +elif PY >= (3, 13): + 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 0 + 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 0 + resume 3 + jump_backward_no_interrupt @loop + send0: + end_send + + yield: + call_intrinsic_1 asm.Intrinsic1Op.INTRINSIC_ASYNC_GEN_WRAP + yield_value 0 + resume 1 + push_null + load_fast $__ddgensend + swap 3 + 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 0 + resume 3 + jump_backward_no_interrupt @presend1 + send1: + end_send + pop_top + pop_except + load_const None + return_value + + exc: + pop_top + load_fast $__ddgen + load_attr (False, 'athrow') + push_null + load_const sys.exc_info + push_null + call 0 + call_function_ex 0 + get_awaitable 0 + load_const None + + presend2: + send @send2 + yield_value 0 + 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() diff --git a/ddtrace/internal/wrapping/generators.py b/ddtrace/internal/wrapping/generators.py index 37b762b64e5..74f310cf8a1 100644 --- a/ddtrace/internal/wrapping/generators.py +++ b/ddtrace/internal/wrapping/generators.py @@ -30,7 +30,10 @@ GENERATOR_ASSEMBLY = Assembly() GENERATOR_HEAD_ASSEMBLY = None -if PY >= (3, 14): +if PY >= (3, 15): + raise NotImplementedError("This version of CPython is not supported yet") + +elif PY >= (3, 14): GENERATOR_HEAD_ASSEMBLY = Assembly() GENERATOR_HEAD_ASSEMBLY.parse( r""" @@ -46,8 +49,86 @@ store_fast $__ddgen load_attr $send store_fast $__ddgensend + load_const next + push_null + load_fast_borrow $__ddgen + + loop: + call 1 + tried + + yield: + try @genexit lasti + yield_value 0 + resume 1 push_null + load_fast_borrow $__ddgensend + swap 3 + 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 + load_fast $__ddgen + load_attr $throw + push_null + load_const sys.exc_info + push_null + call 0 + push_null + 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, 13): + 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 load_const next + push_null load_fast $__ddgen loop: @@ -56,12 +137,11 @@ yield: try @genexit lasti - yield_value 3 + yield_value 0 resume 1 push_null - swap 2 load_fast $__ddgensend - swap 2 + swap 3 jump_backward @loop tried @@ -81,13 +161,13 @@ exc: pop_top - push_null load_fast $__ddgen load_attr $throw push_null load_const sys.exc_info + push_null call 0 - call_function_ex + call_function_ex 0 swap 2 pop_except jump_backward @yield diff --git a/tests/internal/test_wrapping.py b/tests/internal/test_wrapping.py index 3610f0d452a..933d868d7e1 100644 --- a/tests/internal/test_wrapping.py +++ b/tests/internal/test_wrapping.py @@ -162,7 +162,6 @@ def f(a, b, c=None): assert not is_wrapped_with(f, second_wrapper) -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") def test_wrap_generator(): channel = [] @@ -184,7 +183,6 @@ def g(): assert list(g()) == list(range(10)) == channel -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") def test_wrap_generator_send(): def wrapper(f, args, kwargs): return f(*args, **kwargs) @@ -211,7 +209,6 @@ def g(): assert list(range(10)) == channel -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") def test_wrap_generator_throw_close(): def wrapper_maker(channel): def wrapper(f, args, kwargs): @@ -254,10 +251,10 @@ def g(): yield 1 wrap(g, wrapper_maker(channel)) - inspect.isgeneratorfunction(g) + assert inspect.isgeneratorfunction(g) gen = g() - inspect.isgenerator(gen) + assert inspect.isgenerator(gen) for _ in range(10): assert next(gen) == 0 @@ -285,7 +282,6 @@ def f(): assert [frame.f_code.co_name for frame in f()[:4]] == ["f", "wrapper", "f", "test_wrap_stack"] -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") @pytest.mark.asyncio async def test_wrap_async_context_manager_exception_on_exit(): def wrapper(f, args, kwargs): @@ -302,7 +298,6 @@ async def g(): await acm.__aexit__(ValueError, None, None) -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") def test_wrap_generator_yield_from(): channel = [] @@ -376,7 +371,6 @@ def wrapper(f, args, kwargs): assert f(1, path="bar", foo="baz") == (1, (), "bar", {"foo": "baz"}) -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") @pytest.mark.asyncio async def test_async_generator(): async def stream(): @@ -413,7 +407,6 @@ async def agwrapper(f, args, kwargs): assert awrapper_called -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") @pytest.mark.asyncio async def test_wrap_async_generator_send(): def wrapper(f, args, kwargs): @@ -446,7 +439,6 @@ async def consume(): await consume() -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") @pytest.mark.asyncio async def test_double_async_for_with_exception(): channel = None @@ -491,7 +483,6 @@ async def stream(): b"".join([_ async for _ in s]) -@pytest.mark.skipif(sys.version_info > (3, 12), reason="segfault on 3.13") @pytest.mark.asyncio async def test_wrap_async_generator_throw_close(): channel = []