Skip to content

Commit 563f14b

Browse files
authored
fix: injection on try block start for CPython>=3.11 (#13792)
We fix an issue with the injection of hooks on a line that contains just a `try:`. In later versions of Python this has become a mere `NOP` placeholder, which we previously ignored. ## Checklist - [ ] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 03b4a82 commit 563f14b

File tree

3 files changed

+50
-7
lines changed

3 files changed

+50
-7
lines changed

ddtrace/internal/bytecode_injection/__init__.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from typing import Tuple # noqa:F401
88

99
from bytecode import Bytecode
10-
from bytecode import Instr
1110

1211
from ddtrace.internal.assembly import Assembly
1312
from ddtrace.internal.compat import PYTHON_VERSION_INFO as PY
@@ -89,8 +88,9 @@ def _inject_hook(code: Bytecode, hook: HookType, lineno: int, arg: Any) -> None:
8988
# occurrences and inject the hook at each of them. An example of when this
9089
# happens is with finally blocks, which are duplicated at the end of the
9190
# bytecode.
92-
locs: Deque[int] = deque()
91+
locs: Deque[Tuple[int, str]] = deque()
9392
last_lineno = None
93+
instrs = set()
9494
for i, instr in enumerate(code):
9595
try:
9696
if instr.lineno == last_lineno:
@@ -99,17 +99,29 @@ def _inject_hook(code: Bytecode, hook: HookType, lineno: int, arg: Any) -> None:
9999
# Some lines might be implemented across multiple instruction
100100
# offsets, and sometimes a NOP is used as a placeholder. We skip
101101
# those to avoid duplicate injections.
102-
if instr.lineno == lineno and instr.name != "NOP":
103-
locs.appendleft(i)
102+
if instr.lineno == lineno:
103+
locs.appendleft((i, instr.name))
104+
instrs.add(instr.name)
104105
except AttributeError:
105106
# pseudo-instruction (e.g. label)
106107
pass
107108

108109
if not locs:
109110
raise InvalidLine("Line %d does not exist or is either blank or a comment" % lineno)
110111

111-
for i in locs:
112-
if isinstance(instr := code[i], Instr) and instr.name.startswith("END_"):
112+
if instrs == {"NOP"}:
113+
# If the line occurs on NOPs only, we instrument only the first one
114+
last_instr = locs.pop()
115+
locs.clear()
116+
locs.append(last_instr)
117+
elif "NOP" in instrs:
118+
# If the line occurs on NOPs and other instructions, we remove the NOPs
119+
# to avoid injecting the hook multiple times. The NOP in this case is
120+
# just a placeholder.
121+
locs = deque((i, instr) for i, instr in locs if instr != "NOP")
122+
123+
for i, instr in locs:
124+
if instr.startswith("END_"):
113125
# This is the end of a block, e.g. a for loop. We have already
114126
# instrumented the block on entry, so we skip instrumenting the
115127
# end as well.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
dynamic instrumentation: fixed an issue that prevented line probes from
5+
being instrumented on a line containing just the code ``try:`` for CPython
6+
3.11 and later.

tests/internal/bytecode_injection/test_injection.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ def injected_hook(f, hook, arg, line=None):
1919
if line is None:
2020
line = min(linenos(f))
2121

22-
inject_hook(f, hook, line, arg)
22+
try:
23+
inject_hook(f, hook, line, arg)
24+
except InvalidLine:
25+
import dis
26+
27+
dis.dis(f)
28+
raise
2329

2430
yield f
2531

@@ -255,6 +261,25 @@ def test_finally():
255261
hook.assert_called_once_with(arg)
256262

257263

264+
def test_try_except():
265+
def target():
266+
try:
267+
response = 42
268+
for a in range(100):
269+
if a == response:
270+
break
271+
except ValueError as e:
272+
if not e.args:
273+
raise
274+
275+
hook, arg = mock.Mock(), mock.Mock()
276+
277+
with injected_hook(target, hook, arg, line=target.__code__.co_firstlineno + 1):
278+
target()
279+
280+
hook.assert_called_once_with(arg)
281+
282+
258283
def test_for_block():
259284
def for_loop():
260285
a = []

0 commit comments

Comments
 (0)