Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 0 additions & 53 deletions .riot/requirements/14a7a5f.txt

This file was deleted.

53 changes: 0 additions & 53 deletions .riot/requirements/1f96562.txt

This file was deleted.

8 changes: 8 additions & 0 deletions ddtrace/contrib/internal/pytest/_plugin_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/internal/bytecode_injection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Comment on lines 32 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject unsupported hook injection on 3.15

When PY >= (3, 15), this leaves INJECTION_ASSEMBLY and _INJECT_HOOK_OPCODES empty, but inject_hook()/inject_hooks() still splice that empty assembly and report success. Callers such as SCA's Instrumenter.instrument() mark a target instrumented immediately after inject_hook(...), so on Python 3.15 SCA/debugging probes can be reported as installed while no hook will ever fire. Please gate or fail the injection APIs on unsupported versions instead of silently succeeding.

Useful? React with 👍 / 👎.

elif PY >= (3, 13):
INJECTION_ASSEMBLY.parse(
r"""
Expand Down
13 changes: 13 additions & 0 deletions ddtrace/internal/coverage/instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions ddtrace/internal/coverage/instrumentation_py3_12.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/internal/coverage/instrumentation_py3_16.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion ddtrace/internal/wrapping/asyncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Comment on lines 714 to +718

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject async wrapping instead of returning early

On Python 3.15 this returns to wrap_bytecode() after it has already emitted the generic CALL_RETURN trampoline, and wrap() still preserves the original coroutine/async-generator flags. For async targets wrapped by integrations such as pymongo async methods, the resulting coroutine returns the wrapper coroutine object instead of awaiting it (and async generators similarly do not yield through the wrapper), so enabled instrumentation breaks rather than no-ops. Please make wrap() reject/skip unsupported async targets or keep the await/yield adapter.

Useful? React with 👍 / 👎.

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)
Expand Down
16 changes: 14 additions & 2 deletions ddtrace/internal/wrapping/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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__
Expand Down
5 changes: 4 additions & 1 deletion ddtrace/internal/wrapping/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions ddtrace/testing/internal/pytest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 3 additions & 1 deletion riotfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 0 additions & 8 deletions tested_versions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion tests/smoke_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions tests/testing/internal/pytest/test_pytest_itr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="""
Expand Down Expand Up @@ -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="""
Expand Down
6 changes: 6 additions & 0 deletions tests/testing/test_integration.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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."""
Expand Down
Loading