From 08882995e8fc0c5ad1842898cf46b305f271721f Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 00:24:10 -0400 Subject: [PATCH 01/18] ci(riot): schedule Python 3.15 jobs for a starter set List 3.15 in SUPPORTED_PYTHON_VERSIONS so select_pys can opt in. Default MAX stays 3.14. Smoke and sourcecode opt in. --- .riot/requirements/b388a93.txt | 19 +++++++++++++++++++ .riot/requirements/c2ce19f.txt | 23 +++++++++++++++++++++++ riotfile.py | 16 +++++++++++++--- 3 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 .riot/requirements/b388a93.txt create mode 100644 .riot/requirements/c2ce19f.txt diff --git a/.riot/requirements/b388a93.txt b/.riot/requirements/b388a93.txt new file mode 100644 index 00000000000..3b12d3eba9b --- /dev/null +++ b/.riot/requirements/b388a93.txt @@ -0,0 +1,19 @@ +# +# This file is autogenerated by pip-compile with Python 3.15 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/b388a93.in +# +attrs==26.1.0 +coverage[toml]==7.15.4 +hypothesis==6.45.0 +iniconfig==2.3.0 +mock==5.2.0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +pygments==2.21.0 +pytest==9.1.1 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +sortedcontainers==2.4.0 diff --git a/.riot/requirements/c2ce19f.txt b/.riot/requirements/c2ce19f.txt new file mode 100644 index 00000000000..14b40fc0b75 --- /dev/null +++ b/.riot/requirements/c2ce19f.txt @@ -0,0 +1,23 @@ +# +# This file is autogenerated by pip-compile with Python 3.15 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/c2ce19f.in +# +attrs==26.1.0 +coverage[toml]==7.15.4 +hypothesis==6.45.0 +iniconfig==2.3.0 +mock==5.2.0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +pygments==2.21.0 +pytest==9.1.1 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +pytest-randomly==4.1.0 +sortedcontainers==2.4.0 + +# The following packages are considered to be unsafe in a requirements file: +setuptools==84.0.0 diff --git a/riotfile.py b/riotfile.py index d09aa1640a8..e05221bc47d 100644 --- a/riotfile.py +++ b/riotfile.py @@ -23,6 +23,7 @@ (3, 12), (3, 13), (3, 14), + (3, 15), ] @@ -41,6 +42,8 @@ def version_to_str(version: tuple[int, int]) -> str: '3.13' >>> version_to_str((3, 14)) '3.14' + >>> version_to_str((3, 15)) + '3.15' >>> version_to_str((3, )) '3' """ @@ -62,6 +65,8 @@ def str_to_version(version: str) -> tuple[int, int]: (3, 13) >>> str_to_version("3.14") (3, 14) + >>> str_to_version("3.15") + (3, 15) >>> str_to_version("3") (3,) """ @@ -69,7 +74,10 @@ def str_to_version(version: str) -> tuple[int, int]: MIN_PYTHON_VERSION = version_to_str(min(SUPPORTED_PYTHON_VERSIONS)) -MAX_PYTHON_VERSION = version_to_str(max(SUPPORTED_PYTHON_VERSIONS)) +# 3.15 is listed so select_pys(max_version="3.15") can opt in. Default stays +# 3.14 so uncapped suites do not mix 3.15 hashes into 3.9-3.14 --exitfirst jobs. +# Wrap-heavy suites stay at the default until wrap() is live on 3.15. +MAX_PYTHON_VERSION = "3.14" def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYTHON_VERSION) -> list[str]: @@ -83,6 +91,8 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT [] >>> select_pys(min_version='3.9', max_version='3.10') ['3.9', '3.10'] + >>> select_pys(max_version='3.15') + ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] """ min_version = str_to_version(min_version) max_version = str_to_version(max_version) @@ -774,7 +784,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT Venv( name="smoke_test", command="python tests/smoke_test.py {cmdargs}", - pys=select_pys(), + pys=select_pys(max_version="3.15"), ), Venv( name="ddtracerun", @@ -3665,7 +3675,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT "DD_TRACE_PY_ENABLE_ITR_TEST_SKIPPING_FOR_JOB": "true", }, command="pytest {cmdargs} tests/sourcecode", - pys=select_pys(), + pys=select_pys(max_version="3.15"), pkgs={ "setuptools": latest, "pytest-randomly": latest, From 5afa466b7a76d2ececfe77ec13bf80ef5dabf7a6 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 13:17:08 -0400 Subject: [PATCH 02/18] downgrade sourcecode due to errors --- riotfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riotfile.py b/riotfile.py index e05221bc47d..222e0742c20 100644 --- a/riotfile.py +++ b/riotfile.py @@ -3675,7 +3675,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT "DD_TRACE_PY_ENABLE_ITR_TEST_SKIPPING_FOR_JOB": "true", }, command="pytest {cmdargs} tests/sourcecode", - pys=select_pys(max_version="3.15"), + pys=select_pys(), pkgs={ "setuptools": latest, "pytest-randomly": latest, From e4c5b6e249f7d80fd13af9b8ec4f78ea248db309 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 13:23:53 -0400 Subject: [PATCH 03/18] fix(ci): restore Python 3.15 sourcecode coverage imports Defer the bytecode-only injection assembly import and no-op that unsupported helper on Python 3.15, allowing sourcecode instrumentation dependencies to load on the monitoring path. Opt the sourcecode and ddtrace_api suites into 3.15 while keeping the global Python cap at 3.14. --- .riot/requirements/f45a5dd.txt | 25 +++++++++++++++++++ .../coverage/import_instrumentation_py3_12.py | 10 +++++++- riotfile.py | 4 +-- 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 .riot/requirements/f45a5dd.txt diff --git a/.riot/requirements/f45a5dd.txt b/.riot/requirements/f45a5dd.txt new file mode 100644 index 00000000000..41fd9520079 --- /dev/null +++ b/.riot/requirements/f45a5dd.txt @@ -0,0 +1,25 @@ +# +# This file is autogenerated by pip-compile with Python 3.15 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/f45a5dd.in +# +attrs==25.3.0 +certifi==2025.8.3 +charset-normalizer==3.4.3 +coverage[toml]==7.10.5 +ddtrace-api==0.0.1 +hypothesis==6.45.0 +idna==3.10 +iniconfig==2.1.0 +mock==5.2.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +pygments==2.19.2 +pytest==8.4.1 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +requests==2.32.5 +sortedcontainers==2.4.0 +urllib3==2.5.0 diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index bb0b4eb9fba..552e798d8a1 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -6,8 +6,8 @@ from bytecode import Bytecode from bytecode import Instr -from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY from ddtrace.internal.bytecode_injection import HookType +from ddtrace.internal.compat import PYTHON_VERSION_INFO as PY ImportName = tuple[str, tuple[str, ...]] @@ -110,6 +110,14 @@ def inject_import_hooks( (event.instruction_index, (0, path, event.import_name), event.line) for event in import_events ] + # INJECTION_ASSEMBLY exists only on the <3.15 bytecode path. This helper is + # unused on 3.15 (_USE_ACCURATE_IMPORTS is False). Keep this defensive + # no-op for callers that exercise the helper directly on 3.15. + if PY >= (3, 15): + return bytecode.to_code() + + from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY + for idx, arg, lineno in reversed(pending_insertions): bytecode[idx + 1 : idx + 1] = INJECTION_ASSEMBLY.bind(dict(hook=hook, arg=arg), lineno=lineno) diff --git a/riotfile.py b/riotfile.py index 222e0742c20..067b394509d 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1031,7 +1031,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT name="ddtrace_api", command="pytest {cmdargs} tests/contrib/ddtrace_api", pkgs={"ddtrace-api": "==0.0.1", "requests": latest}, - pys=select_pys(), + pys=select_pys(max_version="3.15"), ), # Django Python version support # 2.2 3.9 @@ -3675,7 +3675,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT "DD_TRACE_PY_ENABLE_ITR_TEST_SKIPPING_FOR_JOB": "true", }, command="pytest {cmdargs} tests/sourcecode", - pys=select_pys(), + pys=select_pys(max_version="3.15"), pkgs={ "setuptools": latest, "pytest-randomly": latest, From a0372958b38e80e46ca4e066e06a655fdf315b98 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 13:39:33 -0400 Subject: [PATCH 04/18] chore(py-315): mark defensive import no-op for removal --- .../internal/coverage/import_instrumentation_py3_12.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index 552e798d8a1..a1561a9e80b 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -106,16 +106,17 @@ def inject_import_hooks( else: bytecode = code_or_bytecode - pending_insertions: list[PendingImportHook] = [ - (event.instruction_index, (0, path, event.import_name), event.line) for event in import_events - ] - # INJECTION_ASSEMBLY exists only on the <3.15 bytecode path. This helper is # unused on 3.15 (_USE_ACCURATE_IMPORTS is False). Keep this defensive # no-op for callers that exercise the helper directly on 3.15. + # TODO(py-315): remove this defensive no-op after Python 3.15 GA. if PY >= (3, 15): return bytecode.to_code() + pending_insertions: list[PendingImportHook] = [ + (event.instruction_index, (0, path, event.import_name), event.line) for event in import_events + ] + from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY for idx, arg, lineno in reversed(pending_insertions): From f1c0f55c3da716e193251815e9a6d38691973253 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 14:01:00 -0400 Subject: [PATCH 05/18] chore(py-315): defer sourcecode coverage --- .riot/requirements/c2ce19f.txt | 23 ------------------- .../coverage/import_instrumentation_py3_12.py | 11 +-------- riotfile.py | 2 +- 3 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 .riot/requirements/c2ce19f.txt diff --git a/.riot/requirements/c2ce19f.txt b/.riot/requirements/c2ce19f.txt deleted file mode 100644 index 14b40fc0b75..00000000000 --- a/.riot/requirements/c2ce19f.txt +++ /dev/null @@ -1,23 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.15 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate .riot/requirements/c2ce19f.in -# -attrs==26.1.0 -coverage[toml]==7.15.4 -hypothesis==6.45.0 -iniconfig==2.3.0 -mock==5.2.0 -opentracing==2.4.0 -packaging==26.3 -pluggy==1.6.0 -pygments==2.21.0 -pytest==9.1.1 -pytest-cov==7.1.0 -pytest-mock==3.15.1 -pytest-randomly==4.1.0 -sortedcontainers==2.4.0 - -# The following packages are considered to be unsafe in a requirements file: -setuptools==84.0.0 diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index a1561a9e80b..bb0b4eb9fba 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -6,8 +6,8 @@ from bytecode import Bytecode from bytecode import Instr +from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY from ddtrace.internal.bytecode_injection import HookType -from ddtrace.internal.compat import PYTHON_VERSION_INFO as PY ImportName = tuple[str, tuple[str, ...]] @@ -106,19 +106,10 @@ def inject_import_hooks( else: bytecode = code_or_bytecode - # INJECTION_ASSEMBLY exists only on the <3.15 bytecode path. This helper is - # unused on 3.15 (_USE_ACCURATE_IMPORTS is False). Keep this defensive - # no-op for callers that exercise the helper directly on 3.15. - # TODO(py-315): remove this defensive no-op after Python 3.15 GA. - if PY >= (3, 15): - return bytecode.to_code() - pending_insertions: list[PendingImportHook] = [ (event.instruction_index, (0, path, event.import_name), event.line) for event in import_events ] - from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY - for idx, arg, lineno in reversed(pending_insertions): bytecode[idx + 1 : idx + 1] = INJECTION_ASSEMBLY.bind(dict(hook=hook, arg=arg), lineno=lineno) diff --git a/riotfile.py b/riotfile.py index 067b394509d..40f966bbf6f 100644 --- a/riotfile.py +++ b/riotfile.py @@ -3675,7 +3675,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT "DD_TRACE_PY_ENABLE_ITR_TEST_SKIPPING_FOR_JOB": "true", }, command="pytest {cmdargs} tests/sourcecode", - pys=select_pys(max_version="3.15"), + pys=select_pys(), pkgs={ "setuptools": latest, "pytest-randomly": latest, From e5db2920d8c8ccaab538df2bd3a9f08d460664c0 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 14:09:15 -0400 Subject: [PATCH 06/18] chore(ci): enable Python 3.15 support for dramatiq Extends the latest-package dramatiq Riot environment from Python 3.14 through 3.15 (3.9-3.14 coverage preserved), adds the generated 3.15 lockfile, and fixes the Python 3.12+ coverage/bytecode-injection instrumentation module so it imports cleanly under 3.15. Co-Authored-By: Claude Sonnet 5 --- .codespellignorelines | 3 + .riot/requirements/128b106.txt | 57 ++++++++++--------- .riot/requirements/1381214.txt | 6 +- .riot/requirements/14116fa.txt | 6 +- .riot/requirements/17f991e.txt | 12 ++-- .riot/requirements/19508cd.txt | 6 +- .riot/requirements/4112544.txt | 21 +++++++ .riot/requirements/638973a.txt | 6 +- .../coverage/import_instrumentation_py3_12.py | 27 ++++++++- .../coverage/instrumentation_py3_12.py | 36 ++++++++++++ riotfile.py | 6 +- scripts/integration_registry/registry.yaml | 2 +- supported_versions.json | 11 ++++ .../test_import_dependency_tracking.py | 4 ++ .../test_import_instrumentation_py312.py | 17 ++++++ 15 files changed, 171 insertions(+), 49 deletions(-) create mode 100644 .riot/requirements/4112544.txt diff --git a/.codespellignorelines b/.codespellignorelines index 5c099b93d6f..1cbbfeb426e 100644 --- a/.codespellignorelines +++ b/.codespellignorelines @@ -1,3 +1,6 @@ input="thsi si a spelilgn imstkae.", input="thsi si a spelilgn imstkae.", "input": "thsi si a spelilgn imstkae.", + | grep -qi "^fpr:.*:177F4010FE56CA3336300305F1656F24C74CD1D8:$" \ + | grep -qi "^fpr:.*:BC528686B50D79E339D3721CEB3E94ADBE1229CF:$" \ + | grep -qi "^fpr:.*:EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796:$" \ diff --git a/.riot/requirements/128b106.txt b/.riot/requirements/128b106.txt index fc820df04fc..e2a5784d224 100644 --- a/.riot/requirements/128b106.txt +++ b/.riot/requirements/128b106.txt @@ -2,48 +2,49 @@ # This file is autogenerated by pip-compile with Python 3.14 # by the following command: # -# pip-compile --allow-unsafe --no-annotate .riot/requirements/128b106.in +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/128b106.in # -annotated-types==0.7.0 -anyio==4.11.0 -attrs==25.4.0 -boto3==1.40.46 -botocore==1.40.46 -certifi==2025.10.5 -coverage[toml]==7.10.7 -fastapi==0.118.0 +annotated-doc==0.0.5 +annotated-types==0.8.0 +anyio==4.15.0 +attrs==26.1.0 +boto3==1.43.88 +botocore==1.43.88 +certifi==2026.7.22 +coverage[toml]==7.16.0 +fastapi==0.141.1 freezegun==1.5.5 h11==0.16.0 httpcore==1.0.9 httpretty==1.1.4 httpx==0.27.2 hypothesis==6.45.0 -idna==3.10 -iniconfig==2.1.0 -jmespath==1.0.1 +idna==3.19 +iniconfig==2.3.0 +jmespath==1.1.0 mock==5.2.0 -msgpack==1.1.1 +msgpack==1.2.2 opentracing==2.4.0 -packaging==25.0 +packaging==26.3 pluggy==1.6.0 -pydantic==2.12.0 -pydantic-core==2.41.1 -pygments==2.19.2 -pytest==8.4.2 -pytest-cov==7.0.0 +pydantic==2.13.5 +pydantic-core==2.46.5 +pygments==2.21.0 +pytest==9.1.1 +pytest-cov==7.1.0 pytest-mock==3.15.1 -pytest-randomly==4.0.1 +pytest-randomly==5.0.0 python-dateutil==2.9.0.post0 -s3transfer==0.14.0 +s3transfer==0.19.2 six==1.17.0 sniffio==1.3.1 sortedcontainers==2.4.0 -starlette==0.48.0 -structlog==25.4.0 -typing-extensions==4.15.0 -typing-inspection==0.4.2 -urllib3==2.5.0 -wheel==0.45.1 +starlette==1.6.0 +structlog==26.1.0 +typing-extensions==4.16.0 +typing-inspection==0.4.4 +urllib3==2.7.0 +wheel==0.48.0 # The following packages are considered to be unsafe in a requirements file: -setuptools==80.9.0 +setuptools==84.0.0 diff --git a/.riot/requirements/1381214.txt b/.riot/requirements/1381214.txt index bc356aee9c8..c79f6ac7650 100644 --- a/.riot/requirements/1381214.txt +++ b/.riot/requirements/1381214.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --allow-unsafe --no-annotate .riot/requirements/1381214.in +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/1381214.in # attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 hypothesis==6.45.0 iniconfig==2.3.0 mock==5.2.0 diff --git a/.riot/requirements/14116fa.txt b/.riot/requirements/14116fa.txt index f292a1892e0..9607ca9529c 100644 --- a/.riot/requirements/14116fa.txt +++ b/.riot/requirements/14116fa.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --allow-unsafe --no-annotate .riot/requirements/14116fa.in +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/14116fa.in # attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 hypothesis==6.45.0 iniconfig==2.3.0 mock==5.2.0 diff --git a/.riot/requirements/17f991e.txt b/.riot/requirements/17f991e.txt index 839c9ecd6d8..c4b2032c617 100644 --- a/.riot/requirements/17f991e.txt +++ b/.riot/requirements/17f991e.txt @@ -1,8 +1,12 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile --no-annotate --no-strip-extras --python /opt/homebrew/opt/python@3.14/bin/python3.14 --output-file .riot/requirements/17f991e.txt .riot/requirements/17f991e.in +# +# This file is autogenerated by pip-compile with Python 3.14 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/17f991e.in +# attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 hypothesis==6.45.0 iniconfig==2.3.0 mock==5.2.0 diff --git a/.riot/requirements/19508cd.txt b/.riot/requirements/19508cd.txt index da6d22c812d..a34cde8778c 100644 --- a/.riot/requirements/19508cd.txt +++ b/.riot/requirements/19508cd.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --allow-unsafe --no-annotate .riot/requirements/19508cd.in +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/19508cd.in # attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 hypothesis==6.45.0 iniconfig==2.3.0 mock==5.2.0 diff --git a/.riot/requirements/4112544.txt b/.riot/requirements/4112544.txt new file mode 100644 index 00000000000..6fe5ce1d2f6 --- /dev/null +++ b/.riot/requirements/4112544.txt @@ -0,0 +1,21 @@ +# +# This file is autogenerated by pip-compile with Python 3.15 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/4112544.in +# +attrs==26.1.0 +coverage[toml]==7.15.4 +dramatiq==2.2.0 +hypothesis==6.45.0 +iniconfig==2.3.0 +mock==5.2.0 +opentracing==2.4.0 +packaging==26.3 +pluggy==1.6.0 +pygments==2.21.0 +pytest==9.1.1 +pytest-cov==7.1.0 +pytest-mock==3.15.1 +redis==8.1.0 +sortedcontainers==2.4.0 diff --git a/.riot/requirements/638973a.txt b/.riot/requirements/638973a.txt index 7ad77ba7602..c345f1f3382 100644 --- a/.riot/requirements/638973a.txt +++ b/.riot/requirements/638973a.txt @@ -2,12 +2,12 @@ # This file is autogenerated by pip-compile with Python 3.10 # by the following command: # -# pip-compile --allow-unsafe --no-annotate .riot/requirements/638973a.in +# pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/638973a.in # async-timeout==5.0.1 attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 exceptiongroup==1.3.1 hypothesis==6.45.0 iniconfig==2.3.0 diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index bb0b4eb9fba..f7d4dcdcce9 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -1,15 +1,26 @@ """Import dependency bytecode helpers for Python 3.12+ coverage instrumentation.""" +import sys from types import CodeType import typing as t from bytecode import Bytecode from bytecode import Instr -from ddtrace.internal.bytecode_injection import INJECTION_ASSEMBLY +from ddtrace.internal import bytecode_injection as _bytecode_injection +from ddtrace.internal.assembly import Assembly from ddtrace.internal.bytecode_injection import HookType +_INJECTION_ASSEMBLY: t.Optional[Assembly] = ( + _bytecode_injection.INJECTION_ASSEMBLY if sys.version_info < (3, 15) else None +) +# AIDEV-NOTE: Accurate import-hook injection is intentionally unsupported on 3.15+; see the +# AIDEV-NOTE at instrumentation_py3_12._USE_ACCURATE_IMPORTS for the full rationale and the work +# required to re-enable it (the `bytecode` library's CALL codegen segfaults on 3.15, so injection +# must be reimplemented on sys.monitoring INSTRUCTION events). + + ImportName = tuple[str, tuple[str, ...]] ImportNamesByLine = dict[int, ImportName] CoverageHookArg = tuple[int, str, ImportName] @@ -38,6 +49,13 @@ def _decoded_arg_for_history(instr: Instr) -> t.Any: return 0 +def _decoded_import_name(value: t.Any) -> str: + # AIDEV-NOTE: bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). + if isinstance(value, tuple) and len(value) == 3: + return t.cast(str, value[2]) + return t.cast(str, value) + + def iter_import_events( code_or_bytecode: CodeType | Bytecode, package: str, code: t.Optional[CodeType] = None ) -> list[ImportEvent]: @@ -67,7 +85,7 @@ def iter_import_events( lineno = instr.lineno or code.co_firstlineno if instr.name == "IMPORT_NAME": import_depth = _decoded_import_depth(previous_previous_arg) - current_import_name = t.cast(str, instr.arg) + current_import_name = _decoded_import_name(instr.arg) current_import_package = _resolve_import_package(package, import_depth) events.append(ImportEvent(idx, lineno, (current_import_package, (current_import_name,)))) elif instr.name == "IMPORT_FROM" and current_import_name is not None: @@ -101,6 +119,9 @@ def inject_import_hooks( These injected hooks fire only when the interpreter reaches IMPORT_NAME/IMPORT_FROM, so false runtime branches do not create dependency edges. The hook is inserted after the import opcode, meaning failed imports are not recorded. """ + if _INJECTION_ASSEMBLY is None: + raise NotImplementedError("Accurate import tracking is not supported on Python 3.15+") + if isinstance(code_or_bytecode, CodeType): bytecode = Bytecode.from_code(code_or_bytecode) else: @@ -111,6 +132,6 @@ def inject_import_hooks( ] for idx, arg, lineno in reversed(pending_insertions): - bytecode[idx + 1 : idx + 1] = INJECTION_ASSEMBLY.bind(dict(hook=hook, arg=arg), lineno=lineno) + bytecode[idx + 1 : idx + 1] = _INJECTION_ASSEMBLY.bind(dict(hook=hook, arg=arg), lineno=lineno) return bytecode.to_code() diff --git a/ddtrace/internal/coverage/instrumentation_py3_12.py b/ddtrace/internal/coverage/instrumentation_py3_12.py index 96f33881f9d..e49528b82f7 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/instrumentation_py3_12.py @@ -65,6 +65,42 @@ sys.version.split()[0], ) +# AIDEV-NOTE: Accurate import-hook injection (_DD_COVERAGE_ACCURATE_IMPORTS) is intentionally NOT +# supported on Python 3.15+. It works by splicing a `hook(arg); pop_top` call immediately after each +# IMPORT_NAME/IMPORT_FROM opcode (see import_instrumentation_py3_12.inject_import_hooks) so the hook +# fires only when an import actually executes — failed imports and runtime-false guarded imports are +# not recorded. That splice relies on INJECTION_ASSEMBLY, which ddtrace.internal.bytecode_injection +# only exposes on Python < 3.15 (it switched to a sys.monitoring-based implementation on 3.15); on +# 3.15+ inject_import_hooks raises NotImplementedError. +# +# The static-analysis half (iter_import_events/import_names_by_line) DOES work on 3.15+ thanks to +# _decoded_import_name in import_instrumentation_py3_12, which unpacks PEP 810's bit-packed +# (lazy, eager, name) IMPORT_NAME arg. The remaining blocker is the injection mechanism itself: +# +# The `bytecode` library (0.19.x, the version pinned for 3.15) generates CALL instructions that +# segfault on execution under CPython 3.15.0rc1. The exact INJECTION_ASSEMBLY pattern +# (PUSH_NULL; LOAD_CONST hook; LOAD_CONST arg; CALL 1; POP_TOP) disassembles correctly but +# segfaults on exec for both C-builtins and pure-Python callables; NOP splices and unmodified +# from_code/to_code round-trips work, only CALL faults. This is almost certainly why +# bytecode_injection moved to sys.monitoring on 3.15. A naive re-exposure of INJECTION_ASSEMBLY +# on 3.15 is therefore blocked on an upstream `bytecode` fix (or a hand-rolled raw-bytecode +# emitter, which is fragile and version-specific). +# +# The viable path is to reimplement injection on top of sys.monitoring INSTRUCTION events (3.15 +# exposes a per-instruction event): fire the import hook when the instruction offset immediately +# after IMPORT_NAME/IMPORT_FROM is reached, which preserves the "failed imports not recorded" +# semantics. That requires extending ddtrace.internal.monitoring to multiplex INSTRUCTION (new +# _LOCAL_EVENTS bit, on_instruction handler, dispatcher, register_callback wiring — today it only +# multiplexes PY_START/PY_RETURN/PY_UNWIND/LINE), a per-code-object handler keyed by instruction +# byte offset rather than line, and changing iter_import_events to emit real byte offsets (it +# currently emits a position in the Bytecode list). INSTRUCTION fires on every instruction of an +# instrumented code object, so it should be enabled only for code objects the static scan proves +# contain imports (already computed here), and benchmarked — the conservative static path is the +# default and is unaffected. +# +# Until that work is done, accurate imports stay disabled on 3.15+ and the conservative static +# import tracking in _extract_lines_and_imports is used instead. + EVENT = sys.monitoring.events.PY_START if _USE_FILE_LEVEL_COVERAGE else sys.monitoring.events.LINE # NOTE: We try tool slots in priority order (4, 3, 1) to avoid colliding with other tools. diff --git a/riotfile.py b/riotfile.py index d09aa1640a8..6260ce911a9 100644 --- a/riotfile.py +++ b/riotfile.py @@ -41,6 +41,8 @@ def version_to_str(version: tuple[int, int]) -> str: '3.13' >>> version_to_str((3, 14)) '3.14' + >>> version_to_str((3, 15)) + '3.15' >>> version_to_str((3, )) '3' """ @@ -62,6 +64,8 @@ def str_to_version(version: str) -> tuple[int, int]: (3, 13) >>> str_to_version("3.14") (3, 14) + >>> str_to_version("3.15") + (3, 15) >>> str_to_version("3") (3,) """ @@ -1215,7 +1219,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT }, ), Venv( - pys=select_pys(max_version="3.14"), + pys=select_pys(max_version="3.14") + ["3.15"], pkgs={"dramatiq": latest, "pytest": latest, "redis": latest}, ), ], diff --git a/scripts/integration_registry/registry.yaml b/scripts/integration_registry/registry.yaml index 7e78da99fd8..b1097a45bb1 100644 --- a/scripts/integration_registry/registry.yaml +++ b/scripts/integration_registry/registry.yaml @@ -337,7 +337,7 @@ integrations: tested_versions_by_dependency: dramatiq: min: 1.10.0 - max: 2.2.0 + max: 2.2.1 - integration_name: elasticsearch is_external_package: true diff --git a/supported_versions.json b/supported_versions.json index 50266dd29d8..25cfd48f478 100644 --- a/supported_versions.json +++ b/supported_versions.json @@ -1014,6 +1014,17 @@ ] }, "supportedRange": ">=1.10.0", + "tested": [ + "2.2.1" + ] + }, + { + "testedRuntimes": { + "python": [ + "3.15" + ] + }, + "supportedRange": ">=1.10.0", "tested": [ "2.2.0" ] diff --git a/tests/coverage/test_import_dependency_tracking.py b/tests/coverage/test_import_dependency_tracking.py index 9dc28eacded..ae4e0deb929 100644 --- a/tests/coverage/test_import_dependency_tracking.py +++ b/tests/coverage/test_import_dependency_tracking.py @@ -360,6 +360,10 @@ def test_no_false_dependencies(): @pytest.mark.skipif(sys.version_info < (3, 12), reason="Test specific to Python 3.12+ monitoring API") +@pytest.mark.skipif( + sys.version_info >= (3, 15), + reason="Accurate import injection is not supported on Python 3.15+ (uses INJECTION_ASSEMBLY)", +) @pytest.mark.subprocess(parametrize={"_DD_COVERAGE_FILE_LEVEL": ["true"], "_DD_COVERAGE_ACCURATE_IMPORTS": ["1"]}) def test_file_level_false_guarded_import_not_tracked(): """Runtime-false import statements should not create file-level dependency edges.""" diff --git a/tests/coverage/test_import_instrumentation_py312.py b/tests/coverage/test_import_instrumentation_py312.py index 0acc7cce344..dae4b2902f3 100644 --- a/tests/coverage/test_import_instrumentation_py312.py +++ b/tests/coverage/test_import_instrumentation_py312.py @@ -21,6 +21,9 @@ def hook(arg): return seen +@pytest.mark.skipif( + sys.version_info >= (3, 15), reason="Accurate import hook injection is not supported on Python 3.15+" +) def test_import_hook_injection_skips_runtime_false_import(): seen = _exec_with_import_hooks("RUNTIME_FALSE = bool(0)\nif RUNTIME_FALSE:\n import math\nimport json\n") @@ -28,6 +31,9 @@ def test_import_hook_injection_skips_runtime_false_import(): assert (0, "", ("", ("math",))) not in seen +@pytest.mark.skipif( + sys.version_info >= (3, 15), reason="Accurate import hook injection is not supported on Python 3.15+" +) def test_import_hook_injection_tracks_function_local_import_only_when_called(): from ddtrace.internal.coverage.import_instrumentation_py3_12 import inject_import_hooks from ddtrace.internal.coverage.import_instrumentation_py3_12 import iter_import_events @@ -56,6 +62,17 @@ def hook(arg): assert (0, "", ("", ("decimal",))) in seen +@pytest.mark.skipif(sys.version_info < (3, 15), reason="Accurate import hook injection is supported before Python 3.15") +def test_import_hook_injection_is_not_supported(): + from ddtrace.internal.coverage.import_instrumentation_py3_12 import inject_import_hooks + from ddtrace.internal.coverage.import_instrumentation_py3_12 import iter_import_events + + code = compile("import json\n", "", "exec") + + with pytest.raises(NotImplementedError, match="Accurate import tracking is not supported on Python 3.15\\+"): + inject_import_hooks(code, lambda _: None, "", iter_import_events(code, "")) + + def test_import_event_extraction_groups_from_imports_by_line(): from ddtrace.internal.coverage.import_instrumentation_py3_12 import import_names_by_line from ddtrace.internal.coverage.import_instrumentation_py3_12 import iter_import_events From efac45ff821491b6639d9f949a69af50efb32f37 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 14:20:39 -0400 Subject: [PATCH 07/18] chore(ci): drop unrelated GPG fingerprint codespell ignores These lines are leftovers from an unrelated PR stack and don't belong in the dramatiq Python 3.15 support change. --- .codespellignorelines | 3 --- 1 file changed, 3 deletions(-) diff --git a/.codespellignorelines b/.codespellignorelines index 1cbbfeb426e..5c099b93d6f 100644 --- a/.codespellignorelines +++ b/.codespellignorelines @@ -1,6 +1,3 @@ input="thsi si a spelilgn imstkae.", input="thsi si a spelilgn imstkae.", "input": "thsi si a spelilgn imstkae.", - | grep -qi "^fpr:.*:177F4010FE56CA3336300305F1656F24C74CD1D8:$" \ - | grep -qi "^fpr:.*:BC528686B50D79E339D3721CEB3E94ADBE1229CF:$" \ - | grep -qi "^fpr:.*:EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796:$" \ From 9d2bd7fa89f6c193c755b2a09bf3dfea00c49e42 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 14:27:56 -0400 Subject: [PATCH 08/18] chore(ci): downgrade 3.15 import-hook notes to plain comments These weren't meant as durable AIDEV anchors, just explanatory comments. --- .../internal/coverage/import_instrumentation_py3_12.py | 8 ++++---- ddtrace/internal/coverage/instrumentation_py3_12.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index f7d4dcdcce9..bbbac0e9410 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -15,10 +15,10 @@ _INJECTION_ASSEMBLY: t.Optional[Assembly] = ( _bytecode_injection.INJECTION_ASSEMBLY if sys.version_info < (3, 15) else None ) -# AIDEV-NOTE: Accurate import-hook injection is intentionally unsupported on 3.15+; see the -# AIDEV-NOTE at instrumentation_py3_12._USE_ACCURATE_IMPORTS for the full rationale and the work -# required to re-enable it (the `bytecode` library's CALL codegen segfaults on 3.15, so injection -# must be reimplemented on sys.monitoring INSTRUCTION events). +# Accurate import-hook injection is intentionally unsupported on 3.15+; see the comment at +# instrumentation_py3_12._USE_ACCURATE_IMPORTS for the full rationale and the work required to +# re-enable it (the `bytecode` library's CALL codegen segfaults on 3.15, so injection must be +# reimplemented on sys.monitoring INSTRUCTION events). ImportName = tuple[str, tuple[str, ...]] diff --git a/ddtrace/internal/coverage/instrumentation_py3_12.py b/ddtrace/internal/coverage/instrumentation_py3_12.py index e49528b82f7..6cfd1a6ddaf 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/instrumentation_py3_12.py @@ -65,7 +65,7 @@ sys.version.split()[0], ) -# AIDEV-NOTE: Accurate import-hook injection (_DD_COVERAGE_ACCURATE_IMPORTS) is intentionally NOT +# Accurate import-hook injection (_DD_COVERAGE_ACCURATE_IMPORTS) is intentionally NOT # supported on Python 3.15+. It works by splicing a `hook(arg); pop_top` call immediately after each # IMPORT_NAME/IMPORT_FROM opcode (see import_instrumentation_py3_12.inject_import_hooks) so the hook # fires only when an import actually executes — failed imports and runtime-false guarded imports are From 8c7799f2a95e481dc573127839924cc6772eda83 Mon Sep 17 00:00:00 2001 From: Vlad Scherbich Date: Fri, 4 Sep 2026 15:05:52 -0400 Subject: [PATCH 09/18] chore(py-315): defer ddtrace api coverage --- .riot/requirements/f45a5dd.txt | 25 ------------------------- riotfile.py | 2 +- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 .riot/requirements/f45a5dd.txt diff --git a/.riot/requirements/f45a5dd.txt b/.riot/requirements/f45a5dd.txt deleted file mode 100644 index 41fd9520079..00000000000 --- a/.riot/requirements/f45a5dd.txt +++ /dev/null @@ -1,25 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.15 -# by the following command: -# -# pip-compile --allow-unsafe --no-annotate .riot/requirements/f45a5dd.in -# -attrs==25.3.0 -certifi==2025.8.3 -charset-normalizer==3.4.3 -coverage[toml]==7.10.5 -ddtrace-api==0.0.1 -hypothesis==6.45.0 -idna==3.10 -iniconfig==2.1.0 -mock==5.2.0 -opentracing==2.4.0 -packaging==25.0 -pluggy==1.6.0 -pygments==2.19.2 -pytest==8.4.1 -pytest-cov==6.2.1 -pytest-mock==3.14.1 -requests==2.32.5 -sortedcontainers==2.4.0 -urllib3==2.5.0 diff --git a/riotfile.py b/riotfile.py index 40f966bbf6f..222e0742c20 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1031,7 +1031,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT name="ddtrace_api", command="pytest {cmdargs} tests/contrib/ddtrace_api", pkgs={"ddtrace-api": "==0.0.1", "requests": latest}, - pys=select_pys(max_version="3.15"), + pys=select_pys(), ), # Django Python version support # 2.2 3.9 From 9f288c0b53af7010e2a3116879607ad618fb8dc5 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 15:12:58 -0400 Subject: [PATCH 10/18] Update ddtrace/internal/coverage/import_instrumentation_py3_12.py Co-authored-by: Vlad Scherbich --- ddtrace/internal/coverage/import_instrumentation_py3_12.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index bbbac0e9410..f537349cc5d 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -50,7 +50,7 @@ def _decoded_arg_for_history(instr: Instr) -> t.Any: def _decoded_import_name(value: t.Any) -> str: - # AIDEV-NOTE: bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). + # TODO(py-315): bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). if isinstance(value, tuple) and len(value) == 3: return t.cast(str, value[2]) return t.cast(str, value) From a140af8014d1006d7005d20bc537d9e32a0926a5 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 15:14:18 -0400 Subject: [PATCH 11/18] chore(ci): mark 3.15 import-hook injection gap with TODO(py-315) --- .../internal/coverage/import_instrumentation_py3_12.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index f537349cc5d..4ddf3af18ae 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -15,10 +15,10 @@ _INJECTION_ASSEMBLY: t.Optional[Assembly] = ( _bytecode_injection.INJECTION_ASSEMBLY if sys.version_info < (3, 15) else None ) -# Accurate import-hook injection is intentionally unsupported on 3.15+; see the comment at -# instrumentation_py3_12._USE_ACCURATE_IMPORTS for the full rationale and the work required to -# re-enable it (the `bytecode` library's CALL codegen segfaults on 3.15, so injection must be -# reimplemented on sys.monitoring INSTRUCTION events). +# TODO(py-315): Accurate import-hook injection is intentionally unsupported on 3.15+; see the +# comment at instrumentation_py3_12._USE_ACCURATE_IMPORTS for the full rationale and the work +# required to re-enable it (the `bytecode` library's CALL codegen segfaults on 3.15, so injection +# must be reimplemented on sys.monitoring INSTRUCTION events). ImportName = tuple[str, tuple[str, ...]] From 0f524fe15a5c26579364842ec6f767d6ef213357 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 15:18:25 -0400 Subject: [PATCH 12/18] Update riotfile.py Co-authored-by: Vlad Scherbich --- riotfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riotfile.py b/riotfile.py index 6260ce911a9..55c794ac104 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1219,7 +1219,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT }, ), Venv( - pys=select_pys(max_version="3.14") + ["3.15"], + pys=select_pys(max_version="3.15"), pkgs={"dramatiq": latest, "pytest": latest, "redis": latest}, ), ], From 5c27968e540a3f177c2dacb345ae4d0677c0fdcd Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 15:18:57 -0400 Subject: [PATCH 13/18] Update ddtrace/internal/coverage/import_instrumentation_py3_12.py Co-authored-by: Vlad Scherbich --- ddtrace/internal/coverage/import_instrumentation_py3_12.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index 4ddf3af18ae..1e9f080a2fa 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -51,9 +51,7 @@ def _decoded_arg_for_history(instr: Instr) -> t.Any: def _decoded_import_name(value: t.Any) -> str: # TODO(py-315): bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). - if isinstance(value, tuple) and len(value) == 3: - return t.cast(str, value[2]) - return t.cast(str, value) + return t.cast(str, value[2]) if isinstance(value, tuple) and len(value) == 3 else t.cast(str, value) def iter_import_events( From 52334f95422e01132b1bd90e776675e3b522398d Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 15:24:46 -0400 Subject: [PATCH 14/18] chore(ci): address remaining py-315 import injection review feedback - inline _decoded_import_name into iter_import_events (single consumer) - slim verbose comment explaining the 3.15 injection gap - mark test skip reason with TODO(py-315) --- .../coverage/import_instrumentation_py3_12.py | 14 +++---- .../coverage/instrumentation_py3_12.py | 42 ++++--------------- .../test_import_dependency_tracking.py | 2 +- 3 files changed, 14 insertions(+), 44 deletions(-) diff --git a/ddtrace/internal/coverage/import_instrumentation_py3_12.py b/ddtrace/internal/coverage/import_instrumentation_py3_12.py index 4ddf3af18ae..668291ef7b5 100644 --- a/ddtrace/internal/coverage/import_instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/import_instrumentation_py3_12.py @@ -49,13 +49,6 @@ def _decoded_arg_for_history(instr: Instr) -> t.Any: return 0 -def _decoded_import_name(value: t.Any) -> str: - # TODO(py-315): bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). - if isinstance(value, tuple) and len(value) == 3: - return t.cast(str, value[2]) - return t.cast(str, value) - - def iter_import_events( code_or_bytecode: CodeType | Bytecode, package: str, code: t.Optional[CodeType] = None ) -> list[ImportEvent]: @@ -85,7 +78,12 @@ def iter_import_events( lineno = instr.lineno or code.co_firstlineno if instr.name == "IMPORT_NAME": import_depth = _decoded_import_depth(previous_previous_arg) - current_import_name = _decoded_import_name(instr.arg) + # TODO(py-315): bytecode 0.19 decodes Python 3.15 IMPORT_NAME args to (lazy, eager, name). + current_import_name = ( + t.cast(str, instr.arg[2]) + if isinstance(instr.arg, tuple) and len(instr.arg) == 3 + else t.cast(str, instr.arg) + ) current_import_package = _resolve_import_package(package, import_depth) events.append(ImportEvent(idx, lineno, (current_import_package, (current_import_name,)))) elif instr.name == "IMPORT_FROM" and current_import_name is not None: diff --git a/ddtrace/internal/coverage/instrumentation_py3_12.py b/ddtrace/internal/coverage/instrumentation_py3_12.py index 6cfd1a6ddaf..7fb671fddc6 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/instrumentation_py3_12.py @@ -65,41 +65,13 @@ sys.version.split()[0], ) -# Accurate import-hook injection (_DD_COVERAGE_ACCURATE_IMPORTS) is intentionally NOT -# supported on Python 3.15+. It works by splicing a `hook(arg); pop_top` call immediately after each -# IMPORT_NAME/IMPORT_FROM opcode (see import_instrumentation_py3_12.inject_import_hooks) so the hook -# fires only when an import actually executes — failed imports and runtime-false guarded imports are -# not recorded. That splice relies on INJECTION_ASSEMBLY, which ddtrace.internal.bytecode_injection -# only exposes on Python < 3.15 (it switched to a sys.monitoring-based implementation on 3.15); on -# 3.15+ inject_import_hooks raises NotImplementedError. -# -# The static-analysis half (iter_import_events/import_names_by_line) DOES work on 3.15+ thanks to -# _decoded_import_name in import_instrumentation_py3_12, which unpacks PEP 810's bit-packed -# (lazy, eager, name) IMPORT_NAME arg. The remaining blocker is the injection mechanism itself: -# -# The `bytecode` library (0.19.x, the version pinned for 3.15) generates CALL instructions that -# segfault on execution under CPython 3.15.0rc1. The exact INJECTION_ASSEMBLY pattern -# (PUSH_NULL; LOAD_CONST hook; LOAD_CONST arg; CALL 1; POP_TOP) disassembles correctly but -# segfaults on exec for both C-builtins and pure-Python callables; NOP splices and unmodified -# from_code/to_code round-trips work, only CALL faults. This is almost certainly why -# bytecode_injection moved to sys.monitoring on 3.15. A naive re-exposure of INJECTION_ASSEMBLY -# on 3.15 is therefore blocked on an upstream `bytecode` fix (or a hand-rolled raw-bytecode -# emitter, which is fragile and version-specific). -# -# The viable path is to reimplement injection on top of sys.monitoring INSTRUCTION events (3.15 -# exposes a per-instruction event): fire the import hook when the instruction offset immediately -# after IMPORT_NAME/IMPORT_FROM is reached, which preserves the "failed imports not recorded" -# semantics. That requires extending ddtrace.internal.monitoring to multiplex INSTRUCTION (new -# _LOCAL_EVENTS bit, on_instruction handler, dispatcher, register_callback wiring — today it only -# multiplexes PY_START/PY_RETURN/PY_UNWIND/LINE), a per-code-object handler keyed by instruction -# byte offset rather than line, and changing iter_import_events to emit real byte offsets (it -# currently emits a position in the Bytecode list). INSTRUCTION fires on every instruction of an -# instrumented code object, so it should be enabled only for code objects the static scan proves -# contain imports (already computed here), and benchmarked — the conservative static path is the -# default and is unaffected. -# -# Until that work is done, accurate imports stay disabled on 3.15+ and the conservative static -# import tracking in _extract_lines_and_imports is used instead. +# TODO(py-315): Accurate import-hook injection (_DD_COVERAGE_ACCURATE_IMPORTS) is unsupported on +# 3.15+ because the `bytecode` library's CALL codegen segfaults on exec under CPython 3.15.0rc1, +# which is what ddtrace.internal.bytecode_injection.INJECTION_ASSEMBLY relies on to splice hook +# calls after import opcodes (see import_instrumentation_py3_12.inject_import_hooks). Re-enabling +# this needs either an upstream `bytecode` fix, or reimplementing injection on sys.monitoring +# INSTRUCTION events. Static import tracking (iter_import_events/import_names_by_line) already +# works on 3.15+ and is used as the fallback. EVENT = sys.monitoring.events.PY_START if _USE_FILE_LEVEL_COVERAGE else sys.monitoring.events.LINE diff --git a/tests/coverage/test_import_dependency_tracking.py b/tests/coverage/test_import_dependency_tracking.py index ae4e0deb929..f178d9e49c9 100644 --- a/tests/coverage/test_import_dependency_tracking.py +++ b/tests/coverage/test_import_dependency_tracking.py @@ -362,7 +362,7 @@ def test_no_false_dependencies(): @pytest.mark.skipif(sys.version_info < (3, 12), reason="Test specific to Python 3.12+ monitoring API") @pytest.mark.skipif( sys.version_info >= (3, 15), - reason="Accurate import injection is not supported on Python 3.15+ (uses INJECTION_ASSEMBLY)", + reason="TODO(py-315): accurate import injection is not supported on Python 3.15+ (uses INJECTION_ASSEMBLY)", ) @pytest.mark.subprocess(parametrize={"_DD_COVERAGE_FILE_LEVEL": ["true"], "_DD_COVERAGE_ACCURATE_IMPORTS": ["1"]}) def test_file_level_false_guarded_import_not_tracked(): From 06c5be152134d4b4126176c5c022c5f3411c5883 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 16:10:49 -0400 Subject: [PATCH 15/18] chore(ci): mark remaining 3.15 accurate-import test skips with TODO(py-315) --- tests/coverage/test_import_instrumentation_py312.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/coverage/test_import_instrumentation_py312.py b/tests/coverage/test_import_instrumentation_py312.py index dae4b2902f3..35684cf5494 100644 --- a/tests/coverage/test_import_instrumentation_py312.py +++ b/tests/coverage/test_import_instrumentation_py312.py @@ -22,7 +22,7 @@ def hook(arg): @pytest.mark.skipif( - sys.version_info >= (3, 15), reason="Accurate import hook injection is not supported on Python 3.15+" + sys.version_info >= (3, 15), reason="TODO(py-315): accurate import hook injection is not supported on Python 3.15+" ) def test_import_hook_injection_skips_runtime_false_import(): seen = _exec_with_import_hooks("RUNTIME_FALSE = bool(0)\nif RUNTIME_FALSE:\n import math\nimport json\n") @@ -32,7 +32,7 @@ def test_import_hook_injection_skips_runtime_false_import(): @pytest.mark.skipif( - sys.version_info >= (3, 15), reason="Accurate import hook injection is not supported on Python 3.15+" + sys.version_info >= (3, 15), reason="TODO(py-315): accurate import hook injection is not supported on Python 3.15+" ) def test_import_hook_injection_tracks_function_local_import_only_when_called(): from ddtrace.internal.coverage.import_instrumentation_py3_12 import inject_import_hooks @@ -62,7 +62,9 @@ def hook(arg): assert (0, "", ("", ("decimal",))) in seen -@pytest.mark.skipif(sys.version_info < (3, 15), reason="Accurate import hook injection is supported before Python 3.15") +@pytest.mark.skipif( + sys.version_info < (3, 15), reason="TODO(py-315): accurate import hook injection is supported before Python 3.15" +) def test_import_hook_injection_is_not_supported(): from ddtrace.internal.coverage.import_instrumentation_py3_12 import inject_import_hooks from ddtrace.internal.coverage.import_instrumentation_py3_12 import iter_import_events From bf2cebcf2ddf93b79d19d543ee3bb1251e8607fa Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 16:20:10 -0400 Subject: [PATCH 16/18] chore(ci): simplify dramatiq py315 selection now base includes 3.15 Base is now vlad/py315-riotfile, which adds (3, 15) to SUPPORTED_PYTHON_VERSIONS, so select_pys(max_version="3.15") already covers the full 3.9-3.15 range. --- riotfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riotfile.py b/riotfile.py index 689a958ccaf..f9ee194f140 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1225,7 +1225,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT }, ), Venv( - pys=select_pys(max_version="3.14") + ["3.15"], + pys=select_pys(max_version="3.15"), pkgs={"dramatiq": latest, "pytest": latest, "redis": latest}, ), ], From 2542bca6e3ad26cd95e9d31bbe5de5c8f8437508 Mon Sep 17 00:00:00 2001 From: wantsui Date: Fri, 4 Sep 2026 17:09:13 -0400 Subject: [PATCH 17/18] chore(ci): refresh stale dramatiq py3.15 lock to pick up dramatiq 2.2.1 The lock generated when 3.15 support was added still pinned dramatiq 2.2.0, one release behind the 2.2.1 lock used on 3.9-3.14. Flagged by Datadog Autotest. --- .riot/requirements/4112544.txt | 4 ++-- supported_versions.json | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.riot/requirements/4112544.txt b/.riot/requirements/4112544.txt index 6fe5ce1d2f6..dad74930557 100644 --- a/.riot/requirements/4112544.txt +++ b/.riot/requirements/4112544.txt @@ -5,8 +5,8 @@ # pip-compile --allow-unsafe --no-annotate --no-index .riot/requirements/4112544.in # attrs==26.1.0 -coverage[toml]==7.15.4 -dramatiq==2.2.0 +coverage[toml]==7.16.0 +dramatiq==2.2.1 hypothesis==6.45.0 iniconfig==2.3.0 mock==5.2.0 diff --git a/supported_versions.json b/supported_versions.json index 25cfd48f478..a46e771b845 100644 --- a/supported_versions.json +++ b/supported_versions.json @@ -1010,23 +1010,13 @@ "3.11", "3.12", "3.13", - "3.14" - ] - }, - "supportedRange": ">=1.10.0", - "tested": [ - "2.2.1" - ] - }, - { - "testedRuntimes": { - "python": [ + "3.14", "3.15" ] }, "supportedRange": ">=1.10.0", "tested": [ - "2.2.0" + "2.2.1" ] } ] From a811905cdedd161f583f5924752448f27e6f2171 Mon Sep 17 00:00:00 2001 From: wantsui Date: Tue, 8 Sep 2026 11:02:38 -0400 Subject: [PATCH 18/18] fix(ci): declare Python 3.15 for dramatiq in suitespec matrix riotfile.py's dramatiq Venv was extended to Python 3.15, but the matching suitespec.yml matrix variant wasn't updated, so test_uv_suitespec_matches_riot found a riot-generated 3.15 lockfile with no corresponding suitespec entry. Co-Authored-By: Claude Sonnet 5 --- tests/contrib/suitespec.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/contrib/suitespec.yml b/tests/contrib/suitespec.yml index 2dbca3183d4..8114c706b4a 100644 --- a/tests/contrib/suitespec.yml +++ b/tests/contrib/suitespec.yml @@ -1152,7 +1152,7 @@ suites: python: ['3.9'] dependencies: ['dramatiq~=1.10.0', redis, pika] - name: dramatiq - python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', '3.15'] dependencies: [dramatiq, redis] elasticsearch: pattern: ^elasticsearch(?!:opensearch)