diff --git a/.riot/requirements/14a7a5f.txt b/.riot/requirements/14a7a5f.txt deleted file mode 100644 index 47926643d1d..00000000000 --- a/.riot/requirements/14a7a5f.txt +++ /dev/null @@ -1,53 +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/14a7a5f.in -# -annotated-types==0.7.0 -anyio==4.13.0 -attrs==26.1.0 -certifi==2026.5.20 -colorama==0.4.6 -coverage[toml]==7.14.0 -distro==1.9.0 -eval-type-backport==0.3.1 -genai-prices==0.0.61 -griffe==2.0.2 -griffecli==2.0.2 -griffelib==2.0.2 -h11==0.16.0 -httpcore==1.0.9 -httpx==0.28.1 -hypothesis==6.45.0 -idna==3.15 -iniconfig==2.3.0 -jiter==0.15.0 -logfire-api==4.33.0 -mock==5.2.0 -multidict==6.7.1 -openai==2.37.0 -opentelemetry-api==1.42.0 -opentracing==2.4.0 -packaging==26.2 -pluggy==1.6.0 -propcache==0.5.2 -pydantic==2.12.0a1 -pydantic-ai-slim[openai]==0.8.1 -pydantic-core==2.37.2 -pydantic-graph==0.8.1 -pygments==2.20.0 -pytest==9.0.3 -pytest-asyncio==1.3.0 -pytest-cov==7.1.0 -pytest-mock==3.15.1 -pyyaml==6.0.3 -sniffio==1.3.1 -sortedcontainers==2.4.0 -tqdm==4.67.3 -typing-extensions==4.15.0 -typing-inspection==0.4.2 -urllib3==2.7.0 -vcrpy==7.0.0 -wrapt==2.1.2 -yarl==1.24.2 diff --git a/.riot/requirements/1f96562.txt b/.riot/requirements/1f96562.txt deleted file mode 100644 index cb0e3c5dec7..00000000000 --- a/.riot/requirements/1f96562.txt +++ /dev/null @@ -1,53 +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/1f96562.in -# -annotated-types==0.7.0 -anyio==4.13.0 -attrs==26.1.0 -certifi==2026.5.20 -colorama==0.4.6 -coverage[toml]==7.14.0 -distro==1.9.0 -eval-type-backport==0.3.1 -genai-prices==0.0.61 -griffe==2.0.2 -griffecli==2.0.2 -griffelib==2.0.2 -h11==0.16.0 -httpcore==1.0.9 -httpx==0.28.1 -hypothesis==6.45.0 -idna==3.15 -iniconfig==2.3.0 -jiter==0.15.0 -logfire-api==4.33.0 -mock==5.2.0 -multidict==6.7.1 -openai==2.37.0 -opentelemetry-api==1.42.0 -opentracing==2.4.0 -packaging==26.2 -pluggy==1.6.0 -propcache==0.5.2 -pydantic==2.12.0a1 -pydantic-ai-slim[openai]==1.0.0 -pydantic-core==2.37.2 -pydantic-graph==1.0.0 -pygments==2.20.0 -pytest==9.0.3 -pytest-asyncio==1.3.0 -pytest-cov==7.1.0 -pytest-mock==3.15.1 -pyyaml==6.0.3 -sniffio==1.3.1 -sortedcontainers==2.4.0 -tqdm==4.67.3 -typing-extensions==4.15.0 -typing-inspection==0.4.2 -urllib3==2.7.0 -vcrpy==7.0.0 -wrapt==2.1.2 -yarl==1.24.2 diff --git a/ddtrace/contrib/internal/pytest/_plugin_v2.py b/ddtrace/contrib/internal/pytest/_plugin_v2.py index b15db745c17..e380236a7f1 100644 --- a/ddtrace/contrib/internal/pytest/_plugin_v2.py +++ b/ddtrace/contrib/internal/pytest/_plugin_v2.py @@ -66,6 +66,7 @@ from ddtrace.internal.ci_visibility.utils import take_over_logger_stream_handler from ddtrace.internal.coverage.code import ModuleCodeCollector from ddtrace.internal.coverage.installer import install as install_coverage +from ddtrace.internal.coverage.instrumentation import deregister_monitoring from ddtrace.internal.logger import get_logger from ddtrace.internal.settings import env from ddtrace.internal.settings.asm import config as asm_config @@ -507,6 +508,13 @@ def pytest_unconfigure(config: pytest_Config) -> None: def pytest_sessionstart(session: pytest.Session) -> None: # Reset stale coverage state from any previous in-process session (e.g. pytester.inline_run) reset_coverage_state() + # If pytest-cov is enabled for this session, release sys.monitoring.COVERAGE_ID now so that + # pytest-cov can claim it in pytest_load_initial_conftests (which runs after sessionstart). + # This only matters when a previous in-process session left us holding COVERAGE_ID; in a fresh + # process ddtrace registers lazily during test execution, so pytest-cov always wins the race. + # If we need COVERAGE_ID again later, instrument_all_lines() re-registers it lazily. + if _is_pytest_cov_enabled(session.config): + deregister_monitoring() if not is_test_visibility_enabled(): return diff --git a/ddtrace/internal/bytecode_injection/__init__.py b/ddtrace/internal/bytecode_injection/__init__.py index 661ca193904..97e3cc23341 100644 --- a/ddtrace/internal/bytecode_injection/__init__.py +++ b/ddtrace/internal/bytecode_injection/__init__.py @@ -30,7 +30,7 @@ class InvalidLine(Exception): INJECTION_ASSEMBLY = Assembly() if PY >= (3, 15): - raise NotImplementedError("Python >= 3.15 is not supported yet") + pass # AIDEV-TODO: Populate assembly for 3.15 bytecode (tracked in #17849). elif PY >= (3, 13): INJECTION_ASSEMBLY.parse( r""" diff --git a/ddtrace/internal/coverage/instrumentation.py b/ddtrace/internal/coverage/instrumentation.py index 7dc7fa2b6f8..ea075658177 100644 --- a/ddtrace/internal/coverage/instrumentation.py +++ b/ddtrace/internal/coverage/instrumentation.py @@ -3,12 +3,25 @@ # Import are noqa'd otherwise some formatters will helpfully remove them if sys.version_info >= (3, 16): + from ddtrace.internal.coverage.instrumentation_py3_16 import deregister_monitoring # noqa from ddtrace.internal.coverage.instrumentation_py3_16 import instrument_all_lines # noqa elif sys.version_info >= (3, 12): + from ddtrace.internal.coverage.instrumentation_py3_12 import deregister_monitoring # noqa from ddtrace.internal.coverage.instrumentation_py3_12 import instrument_all_lines # noqa elif sys.version_info >= (3, 11): from ddtrace.internal.coverage.instrumentation_py3_11 import instrument_all_lines # noqa + + def deregister_monitoring() -> None: # noqa: E306 + pass # sys.monitoring not available on Python < 3.12 + elif sys.version_info >= (3, 10): from ddtrace.internal.coverage.instrumentation_py3_10 import instrument_all_lines # noqa + + def deregister_monitoring() -> None: # noqa: E306 + pass # sys.monitoring not available on Python < 3.12 + else: from ddtrace.internal.coverage.instrumentation_py3_9 import instrument_all_lines # noqa + + def deregister_monitoring() -> None: # noqa: E306 + pass # sys.monitoring not available on Python < 3.12 diff --git a/ddtrace/internal/coverage/instrumentation_py3_12.py b/ddtrace/internal/coverage/instrumentation_py3_12.py index 3a394f7a1ce..cadc9cfcf1e 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_12.py +++ b/ddtrace/internal/coverage/instrumentation_py3_12.py @@ -130,6 +130,20 @@ def _register_monitoring(): sys.monitoring.register_callback(sys.monitoring.COVERAGE_ID, EVENT, _event_handler) +def deregister_monitoring() -> None: + """Release sys.monitoring.COVERAGE_ID if currently held by datadog. + + Called at the start of each new in-process pytest session (e.g. pytester.inline_run) + so that another coverage tool such as pytest-cov can claim COVERAGE_ID without + hitting "ValueError: tool 1 is already in use". The next instrumentation call + will re-register us if coverage is still needed. + """ + if sys.monitoring.get_tool(sys.monitoring.COVERAGE_ID) == "datadog": + sys.monitoring.register_callback(sys.monitoring.COVERAGE_ID, EVENT, None) + sys.monitoring.free_tool_id(sys.monitoring.COVERAGE_ID) + _CODE_HOOKS.clear() + + def _instrument_with_monitoring( code: CodeType, hook: HookType, path: str, package: str ) -> tuple[CodeType, CoverageLines]: diff --git a/ddtrace/internal/coverage/instrumentation_py3_16.py b/ddtrace/internal/coverage/instrumentation_py3_16.py index 2c663719e28..550b9193220 100644 --- a/ddtrace/internal/coverage/instrumentation_py3_16.py +++ b/ddtrace/internal/coverage/instrumentation_py3_16.py @@ -18,3 +18,8 @@ def instrument_all_lines(code: CodeType, hook: HookType, path: str, package: str) -> tuple[CodeType, CoverageLines]: # No-op return code, CoverageLines() + + +def deregister_monitoring() -> None: + # No-op: instrumentation is not yet implemented for Python 3.16+ + pass diff --git a/ddtrace/internal/wrapping/asyncs.py b/ddtrace/internal/wrapping/asyncs.py index 06dc4507f12..020733d59e5 100644 --- a/ddtrace/internal/wrapping/asyncs.py +++ b/ddtrace/internal/wrapping/asyncs.py @@ -36,7 +36,7 @@ ASYNC_HEAD_ASSEMBLY = None if PY >= (3, 15): - raise NotImplementedError("This version of CPython is not supported yet") + pass # AIDEV-TODO: Populate assemblies for 3.15 bytecode (tracked in #17849). elif PY >= (3, 14): ASYNC_HEAD_ASSEMBLY = Assembly() @@ -712,6 +712,10 @@ def wrap_async(instrs: list[bc.Instr], code: CodeType, lineno: int) -> None: + if PY >= (3, 15): + # AIDEV-TODO: Async wrapping not yet implemented for 3.15; no-op until + # #17849 lands. Remove this guard once the assemblies above are populated. + return if (bc.CompilerFlags.ASYNC_GENERATOR | bc.CompilerFlags.COROUTINE) & code.co_flags: if ASYNC_HEAD_ASSEMBLY is not None: instrs[0:0] = ASYNC_HEAD_ASSEMBLY.bind(lineno=lineno) diff --git a/ddtrace/internal/wrapping/context.py b/ddtrace/internal/wrapping/context.py index 62043667412..6cc8c54831a 100644 --- a/ddtrace/internal/wrapping/context.py +++ b/ddtrace/internal/wrapping/context.py @@ -79,7 +79,9 @@ CONTEXT_FOOT = Assembly() if sys.version_info >= (3, 15): - raise NotImplementedError("Python >= 3.15 is not supported yet") + # AIDEV-TODO: Populate assemblies for 3.15 bytecode (tracked in #17849). + # Define the stub so references below don't NameError. + CONTEXT_RETURN_CONST: Assembly = Assembly() elif sys.version_info >= (3, 13): CONTEXT_HEAD.parse( r""" @@ -587,7 +589,17 @@ def extract(cls, f: FunctionType) -> "_UniversalWrappingContext": raise ValueError("Function is not wrapped") return t.cast(_UniversalWrappingContext, t.cast(ContextWrappedFunction, f).__dd_context_wrapped__) - if sys.version_info >= (3, 11): + if sys.version_info >= (3, 15): + + def wrap(self) -> None: + # AIDEV-TODO: No-op until #17849 implements wrapping for 3.15. + pass + + def unwrap(self) -> None: + # AIDEV-TODO: No-op until #17849 implements wrapping for 3.15. + pass + + elif sys.version_info >= (3, 11): def wrap(self) -> None: f = self.__wrapped__ diff --git a/ddtrace/internal/wrapping/generators.py b/ddtrace/internal/wrapping/generators.py index 7406f978cbc..8b12b472254 100644 --- a/ddtrace/internal/wrapping/generators.py +++ b/ddtrace/internal/wrapping/generators.py @@ -34,7 +34,7 @@ GENERATOR_HEAD_ASSEMBLY = None if PY >= (3, 15): - raise NotImplementedError("This version of CPython is not supported yet") + pass # AIDEV-TODO: Populate assemblies for 3.15 bytecode (tracked in #17849). elif PY >= (3, 14): GENERATOR_HEAD_ASSEMBLY = Assembly() @@ -485,6 +485,9 @@ def wrap_generator(instrs: list[bc.Instr], code: CodeType, lineno: int) -> None: + if PY >= (3, 15): + # AIDEV-TODO: No-op until #17849 populates the assemblies above. + return if GENERATOR_HEAD_ASSEMBLY is not None: instrs[0:0] = GENERATOR_HEAD_ASSEMBLY.bind(lineno=lineno) diff --git a/ddtrace/testing/internal/pytest/plugin.py b/ddtrace/testing/internal/pytest/plugin.py index f9b3872a821..e29a05b906b 100644 --- a/ddtrace/testing/internal/pytest/plugin.py +++ b/ddtrace/testing/internal/pytest/plugin.py @@ -19,6 +19,7 @@ from ddtrace.contrib.internal.coverage.utils import _is_pytest_cov_enabled from ddtrace.contrib.internal.coverage.utils import handle_coverage_report from ddtrace.internal.ci_visibility.utils import get_source_lines_for_test_method +from ddtrace.internal.coverage.instrumentation import deregister_monitoring from ddtrace.internal.settings import env from ddtrace.internal.utils.inspection import undecorated from ddtrace.testing.internal.ci import CITag @@ -275,6 +276,13 @@ def pytest_sessionstart(self, session: pytest.Session) -> None: if session.config.getoption("ddtrace-patch-all"): self.enable_all_ddtrace_integrations = True + # If pytest-cov is enabled for this session, release sys.monitoring.COVERAGE_ID so + # that pytest-cov can claim it in pytest_load_initial_conftests (which runs after + # sessionstart). Required when a previous in-process session (e.g. pytester.inline_run) + # left us holding the slot; instrument_all_lines() re-registers it lazily if needed. + if _is_pytest_cov_enabled(session.config): + deregister_monitoring() + self.session.start() self.manager.start() diff --git a/riotfile.py b/riotfile.py index 3eb68f7d9b5..30da8c9b4db 100644 --- a/riotfile.py +++ b/riotfile.py @@ -3215,7 +3215,9 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT }, ), Venv( - pys=select_pys(min_version="3.10"), + # pydantic==2.12.0a1 requires pydantic-core~=2.37.x which uses + # PyO3 0.25.x — capped at 3.14 since PyO3 0.25.x doesn't support 3.15+. + pys=select_pys(min_version="3.10", max_version="3.14"), pkgs={ "pydantic-ai-slim[openai]": ["==0.8.1", "==1.0.0"], "pydantic": "==2.12.0a1", diff --git a/tested_versions.json b/tested_versions.json index d97c3589009..47f6b475e94 100644 --- a/tested_versions.json +++ b/tested_versions.json @@ -4047,14 +4047,6 @@ "version": "1.63.0", "python_version": "3.14" }, - { - "version": "0.8.1", - "python_version": "3.15" - }, - { - "version": "1.0.0", - "python_version": "3.15" - }, { "version": "1.63.0", "python_version": "3.15" diff --git a/tests/conftest.py b/tests/conftest.py index ae6c346b181..57df25e04f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -210,7 +210,7 @@ def use_dummy_writer(): def auto_enable_crashtracking(): # Crashtracking is only supported on linux right now # TODO: Default to `True` when Windows and Darwin are supported - yield platform.system() == "Linux" + yield platform.system() == "Linux" and crashtracking.is_available @pytest.fixture(autouse=True) diff --git a/tests/smoke_test.py b/tests/smoke_test.py index ddd71fbeea2..a06da19ca50 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -76,7 +76,8 @@ def emit(self, record): print("Skipping test, 32-bit DDWAF not ready yet") # Profiling smoke test - if platform.system() in ("Linux", "Darwin") and sys.maxsize > (1 << 32): + # Profiling extensions are not built for Python 3.15+ (see setup.py, tracked in #17817) + if platform.system() in ("Linux", "Darwin") and sys.maxsize > (1 << 32) and sys.version_info < (3, 15): print("Running profiling smoke test...") profiling_cmd = [sys.executable, "-c", "import ddtrace.profiling.auto"] result = subprocess.run(profiling_cmd, capture_output=True, text=True) diff --git a/tests/testing/internal/pytest/test_pytest_itr.py b/tests/testing/internal/pytest/test_pytest_itr.py index 809dcd07930..54b483161d6 100644 --- a/tests/testing/internal/pytest/test_pytest_itr.py +++ b/tests/testing/internal/pytest/test_pytest_itr.py @@ -290,7 +290,6 @@ def test_should_run(): assert session["content"]["metrics"]["test.itr.tests_skipping.count"] == 1 @pytest.mark.skipif("slipcover" in sys.modules, reason="slipcover is incompatible with ITR code coverage") - @pytest.mark.skipif(sys.version_info >= (3, 14), reason="ITR code coverage currently not supported in Python 3.14") def test_itr_code_coverage_enabled(self, pytester: Pytester) -> None: pytester.makepyfile( lib_constants=""" @@ -318,7 +317,6 @@ def test_answer(): assert covered_files == {"/test_foo.py", "/lib_constants.py"} @pytest.mark.skipif("slipcover" in sys.modules, reason="slipcover is incompatible with ITR code coverage") - @pytest.mark.skipif(sys.version_info >= (3, 14), reason="ITR code coverage currently not supported in Python 3.14") def test_itr_code_coverage_disabled(self, pytester: Pytester) -> None: pytester.makepyfile( lib_constants=""" diff --git a/tests/testing/test_integration.py b/tests/testing/test_integration.py index 61b1d5c9300..d64e08cbdc6 100644 --- a/tests/testing/test_integration.py +++ b/tests/testing/test_integration.py @@ -1,11 +1,13 @@ #!/usr/bin/env python3 import os +import sys from unittest.mock import Mock from unittest.mock import patch from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +import pytest from ddtrace.testing.internal.session_manager import SessionManager from ddtrace.testing.internal.test_data import ModuleRef @@ -491,6 +493,10 @@ def test_simple_pass(): result.assert_outcomes(passed=2) +@pytest.mark.skipif( + sys.version_info >= (3, 15), + reason="IAST taint-tracking native extension not built for Python 3.15+ (setup.py, tracked in IAST 3.15 issue)", +) class TestIASTTerminalSummary: def test_ddtrace_iast_terminal_summary_enabled(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: """Test that IAST terminal summary is present when IAST is enabled."""