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..dad74930557 --- /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.16.0 +dramatiq==2.2.1 +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..668291ef7b5 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 +) +# 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, ...]] ImportNamesByLine = dict[int, ImportName] CoverageHookArg = tuple[int, str, ImportName] @@ -67,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 = t.cast(str, 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: @@ -101,6 +117,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 +130,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 ba5f0f83b37..688d48ba906 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/instrumentation_py3_12.py @@ -65,6 +65,14 @@ sys.version.split()[0], ) +# 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 # 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 e2ddaf33bd0..5378d00a8a5 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1235,7 +1235,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.15"), pkgs={"dramatiq": latest, "pytest": latest, "redis": latest}, ), ], diff --git a/scripts/integration_registry/registry.yaml b/scripts/integration_registry/registry.yaml index 285638f85bb..7edf0181749 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 14cd699c857..505a1a46477 100644 --- a/supported_versions.json +++ b/supported_versions.json @@ -1020,12 +1020,13 @@ "3.11", "3.12", "3.13", - "3.14" + "3.14", + "3.15" ] }, "supportedRange": ">=1.10.0", "tested": [ - "2.2.0" + "2.2.1" ] } ] diff --git a/tests/contrib/suitespec.yml b/tests/contrib/suitespec.yml index 942009e8947..b407898fd9a 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) diff --git a/tests/coverage/test_import_dependency_tracking.py b/tests/coverage/test_import_dependency_tracking.py index 9dc28eacded..f178d9e49c9 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="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(): """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..35684cf5494 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="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") @@ -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="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 from ddtrace.internal.coverage.import_instrumentation_py3_12 import iter_import_events @@ -56,6 +62,19 @@ def hook(arg): assert (0, "", ("", ("decimal",))) in seen +@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 + + 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