chore(profiling): don't crash apps on non-working imports under Python 3.15 (PROF-15769) - #19724
Conversation
Wheel smoke tests fail on 3.15 because wrapping raises at import and profiling.auto loads native extensions that are not built yet. Defer the wrapping error until wrap is actually used, and disable the profiler when those extensions are missing.
Codeowners resolved asResolved from the full PR diff against No remaining files require a CODEOWNERS review. |
Circular import analysis
|
Dependency direction analysis
|
🎉 All green!🧪 All tests passed 🔗 Commit SHA: 39fc59c | Docs | View more details | Give us feedback! |
BenchmarksBenchmark execution time: 2026-08-21 05:11:31 Comparing candidate commit 39fc59c in PR branch Found 0 performance improvements and 7 performance regressions! Performance is the same for 615 metrics, 10 unstable metrics.
|
… load in smoke test
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fe36aeced2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Skip LazyWrappingContext bytecode wrapping on 3.15 and execute the lazy initializer body in module scope instead, so debugging product entry points load without failing the smoke-test product check. Document the profiling is_available/failure_msg contract in basic_usage.rst. No release note: this is pre-release 3.15 degrade behavior only.
There was a problem hiding this comment.
Pull request overview
This PR improves Python 3.15 “import-time degrade” behavior so dd-trace-py doesn’t crash when unsupported wrapping/bytecode injection paths are imported (but not used) and when profiling native extensions aren’t available yet (e.g., during wheel smoke tests).
Changes:
- Make
ddtrace.internal.wrapping.*andddtrace.internal.bytecode_injectionimport successfully on unsupported Python versions, while still raisingNotImplementedErrorwhen the unsupported functionality is actually invoked. - Add a profiling availability gate (
is_available/failure_msg) toddtrace.profiling, and makeddtrace.profiling.autodegrade with a warning instead of crashing when profiling isn’t available. - Add/extend tests to cover 3.15 import-time degrade behavior and profiling auto-import degradation.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/smoke_test.py |
Restores environment correctly and asserts product plugins didn’t fail during bootstrap/WAF smoke path. |
tests/profiling/test_profiler.py |
Adds a subprocess test ensuring ddtrace.profiling.auto import does not crash when profiling is unavailable. |
tests/internal/test_py315_import_degrade.py |
New tests ensuring wrapping/bytecode injection modules import on 3.15 while still raising when used. |
ddtrace/profiling/bootstrap/sitecustomize.py |
Adds is_available gate to avoid crashing when native profiling extensions are missing. |
ddtrace/profiling/__init__.py |
Introduces is_available/failure_msg pattern and a stub Profiler that raises ImportError on construction when unavailable. |
ddtrace/internal/wrapping/generators.py |
Removes import-time crash on unsupported versions; raises only when wrapping is invoked. |
ddtrace/internal/wrapping/context.py |
Removes import-time crash on unsupported versions; raises only when wrapping is invoked. |
ddtrace/internal/wrapping/asyncs.py |
Removes import-time crash on unsupported versions; raises only when wrapping is invoked. |
ddtrace/internal/wrapping/__init__.py |
Ensures wrap() / wrap_bytecode() raise at call-time rather than import-time on unsupported versions. |
ddtrace/internal/compat.py |
Adds NEXT_PY_VERSION* constants to centralize “first unsupported CPython version” logic. |
ddtrace/internal/bytecode_injection/__init__.py |
Removes import-time crash on unsupported versions; raises only when injection is invoked. |
Suppressed comments (2)
ddtrace/internal/wrapping/init.py:312
- This NotImplementedError message hard-codes
NEXT_PY_VERSION(e.g. "3.15"), which is confusing on later unsupported versions (it will still claim "...: 3.15" on 3.16+). Reword to describe the unsupported range instead of a single version.
"""
if PY >= NEXT_PY_VERSION_INFO:
raise NotImplementedError("This version of CPython is not supported yet: %s" % (NEXT_PY_VERSION,))
ddtrace/internal/bytecode_injection/init.py:232
- This NotImplementedError message hard-codes
NEXT_PY_VERSION(e.g. "3.15"), which is confusing on later unsupported versions (it will still claim "...: 3.15" on 3.16+). Reword to describe the unsupported range instead of a single version.
"""
if PY >= NEXT_PY_VERSION_INFO:
raise NotImplementedError("This version of CPython is not supported yet: %s" % (NEXT_PY_VERSION,))
abstract_code = Bytecode.from_code(f.__code__)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Use a shared unsupported-version message that describes the threshold range (Python 3.15 and later) instead of implying only one version is blocked. Import ddtrace.profiling as a module in sitecustomize so is_available/failure_msg reflect runtime monkeypatches in tests.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
ddtrace/profiling/init.py:24
- The fallback Profiler() ImportError message always claims "native extensions are not built", but the guarded import can fail for other reasons too. Including failure_msg in the exception text makes the error actionable and avoids an inaccurate root-cause statement.
raise ImportError(
"ddtrace.profiling is not available on this Python version "
"(native extensions are not built). "
"Import ddtrace.profiling and check is_available/failure_msg for details."
) from _profiler_import_error
ddtrace/internal/module.py:772
- _exec_lazy_init() executes the parsed function body as a synthetic module. If the lazy initializer function has a docstring, the first string literal statement will be treated as the module docstring when compiled/executed, unexpectedly overwriting module_globals['doc']. Strip a leading docstring expression from the function body before compiling (and fix missing locations).
func_def = tree.body[0]
if not isinstance(func_def, ast.FunctionDef):
raise TypeError("lazy() expects a function definition")
mod = ast.Module(body=func_def.body, type_ignores=[])
exec(compile(mod, f.__code__.co_filename, "exec"), module_globals)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e638fae677
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Execute the compiled @lazy initializer and promote its locals to module scope instead of reconstructing the body via inspect.getsource(), so lazy modules work from sourceless or frozen distributions.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
ddtrace/internal/bytecode_injection/init.py:233
- inject_hook raises NotImplementedError on Python >= NEXT_PY_VERSION_INFO, but eject_hook does not. On unsupported versions this can lead to incorrect behavior since the injected opcode pattern is not defined/parsed. Add the same version guard to eject_hook.
def inject_hook(f: FunctionType, hook: HookType, line: int, arg: Any) -> FunctionType:
"""Inject a hook into a function.
The hook is injected at the given line number and called with the given
argument. The latter is also used as an identifier for the hook. This should
be kept in case the hook needs to be removed.
"""
if PY >= NEXT_PY_VERSION_INFO:
raise NotImplementedError(NEXT_PY_UNSUPPORTED_MSG)
abstract_code = Bytecode.from_code(f.__code__)
ddtrace/internal/bytecode_injection/init.py:188
- inject_hooks/inject_hook correctly raise NotImplementedError on Python >= NEXT_PY_VERSION_INFO. However eject_hooks has no version guard, so it can execute on unsupported versions even though INJECTION_ASSEMBLY/_INJECT_HOOK_OPCODES are not defined for them (they may be empty), which can lead to incorrect ejection behavior or unexpected errors. Add the same NotImplementedError guard to eject_hooks.
This issue also appears on line 223 of the same file.
raise NotImplementedError(NEXT_PY_UNSUPPORTED_MSG)
abstract_code = Bytecode.from_code(get_function_code(f))
failed = []
for hook, line, arg in hooks:
_exec_lazy_init must always return its own trace function so it still receives the return event when pytest or another tracer is already installed via sys.settrace().
Do not forward trace events to an existing pytest/coverage tracer during _exec_lazy_init; those tracers can reinstall themselves on "call" and prevent our handler from seeing the return event. Capture line+return snapshots with dict(frame.f_locals) for PEP 667.
|
/merge |
|
View all feedbacks in Devflow UI.
PR already in the queue with status in_progress |
|
/code blockers |
|
View all feedbacks in Devflow UI.
Checking merge blockers for #19724...
No merge blockers detected. |
Description
Part of the Python 3.15 integration parity effort
(parent tracker: #17809).
Closes #19730
Wheel smoke tests fail on 3.15 because wrapping raises at import (pulled in via
ModuleWatchdog/WrappingContext) andddtrace.profiling.autoloads native extensions that are not built yet. This PR makes those imports succeed and degrades instead of crashing.If this PR merges and nothing else lands:
ddtrace.internal.wrapping(asyncs,generators,context) andddtrace.internal.bytecode_injectionimport successfully on 3.15.wrap(),wrap_bytecode(),wrap_async(),wrap_generator(),WrappingContext.wrap(),inject_hook(), andinject_hooks()still raiseNotImplementedErroruntil chore: wrapping context support for Python 3.15 #17849.ddtrace.profilinguses the sameis_available/failure_msgpattern asddtrace.internal.datadog.profiling.{ddup,stack}. IfProfilercannot be imported (native extensions missing),is_availableisFalseand constructingProfiler()raisesImportError.ddtrace.profiling.autologs a warning and returns whenis_availableisFalse, instead of crashing the process.Checklist
riotfile.pyto a version that supports Python 3.15 — N/A (not an integration bump)max_version="3.13"/"3.14"cap on the affected venv(s) (if present) — N/Ariot generate <suite-pattern>and committed the regenerated.riot/requirements/*.txtlockfiles — N/Ascripts/run-tests <suite>— intended gate is 3.15 wheel smoke tests in CIsupported_versions.jsonif integration min/max versions changed — N/Areleasenotes/notes/Testing