Skip to content

Commit 93b9fd4

Browse files
committed
feat(profiling): Use co_qualname in python 3.11
The `get_frame_name` implementation works well for <3.11 but 3.11 introduced a `co_qualname` that works like our implementation of `get_frame_name` and handles some cases better.
1 parent 20c25f2 commit 93b9fd4

File tree

3 files changed

+78
-63
lines changed

3 files changed

+78
-63
lines changed

sentry_sdk/_compat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PY33 = sys.version_info[0] == 3 and sys.version_info[1] >= 3
1717
PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7
1818
PY310 = sys.version_info[0] == 3 and sys.version_info[1] >= 10
19+
PY311 = sys.version_info[0] == 3 and sys.version_info[1] >= 11
1920

2021
if PY2:
2122
import urlparse

sentry_sdk/profiler.py

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from contextlib import contextmanager
2525

2626
import sentry_sdk
27-
from sentry_sdk._compat import PY33
27+
from sentry_sdk._compat import PY33, PY311
2828
from sentry_sdk._types import MYPY
2929
from sentry_sdk.utils import (
3030
filename_for_module,
@@ -241,55 +241,60 @@ def extract_frame(frame, cwd):
241241
)
242242

243243

244-
def get_frame_name(frame):
245-
# type: (FrameType) -> str
246-
247-
# in 3.11+, there is a frame.f_code.co_qualname that
248-
# we should consider using instead where possible
249-
250-
f_code = frame.f_code
251-
co_varnames = f_code.co_varnames
252-
253-
# co_name only contains the frame name. If the frame was a method,
254-
# the class name will NOT be included.
255-
name = f_code.co_name
256-
257-
# if it was a method, we can get the class name by inspecting
258-
# the f_locals for the `self` argument
259-
try:
260-
if (
261-
# the co_varnames start with the frame's positional arguments
262-
# and we expect the first to be `self` if its an instance method
263-
co_varnames
264-
and co_varnames[0] == "self"
265-
and "self" in frame.f_locals
266-
):
267-
for cls in frame.f_locals["self"].__class__.__mro__:
268-
if name in cls.__dict__:
269-
return "{}.{}".format(cls.__name__, name)
270-
except AttributeError:
271-
pass
272-
273-
# if it was a class method, (decorated with `@classmethod`)
274-
# we can get the class name by inspecting the f_locals for the `cls` argument
275-
try:
276-
if (
277-
# the co_varnames start with the frame's positional arguments
278-
# and we expect the first to be `cls` if its a class method
279-
co_varnames
280-
and co_varnames[0] == "cls"
281-
and "cls" in frame.f_locals
282-
):
283-
for cls in frame.f_locals["cls"].__mro__:
284-
if name in cls.__dict__:
285-
return "{}.{}".format(cls.__name__, name)
286-
except AttributeError:
287-
pass
288-
289-
# nothing we can do if it is a staticmethod (decorated with @staticmethod)
290-
291-
# we've done all we can, time to give up and return what we have
292-
return name
244+
if PY311:
245+
246+
def get_frame_name(frame):
247+
# type: (FrameType) -> str
248+
return frame.f_code.co_qualname # type: ignore
249+
250+
else:
251+
252+
def get_frame_name(frame):
253+
# type: (FrameType) -> str
254+
255+
f_code = frame.f_code
256+
co_varnames = f_code.co_varnames
257+
258+
# co_name only contains the frame name. If the frame was a method,
259+
# the class name will NOT be included.
260+
name = f_code.co_name
261+
262+
# if it was a method, we can get the class name by inspecting
263+
# the f_locals for the `self` argument
264+
try:
265+
if (
266+
# the co_varnames start with the frame's positional arguments
267+
# and we expect the first to be `self` if its an instance method
268+
co_varnames
269+
and co_varnames[0] == "self"
270+
and "self" in frame.f_locals
271+
):
272+
for cls in frame.f_locals["self"].__class__.__mro__:
273+
if name in cls.__dict__:
274+
return "{}.{}".format(cls.__name__, name)
275+
except AttributeError:
276+
pass
277+
278+
# if it was a class method, (decorated with `@classmethod`)
279+
# we can get the class name by inspecting the f_locals for the `cls` argument
280+
try:
281+
if (
282+
# the co_varnames start with the frame's positional arguments
283+
# and we expect the first to be `cls` if its a class method
284+
co_varnames
285+
and co_varnames[0] == "cls"
286+
and "cls" in frame.f_locals
287+
):
288+
for cls in frame.f_locals["cls"].__mro__:
289+
if name in cls.__dict__:
290+
return "{}.{}".format(cls.__name__, name)
291+
except AttributeError:
292+
pass
293+
294+
# nothing we can do if it is a staticmethod (decorated with @staticmethod)
295+
296+
# we've done all we can, time to give up and return what we have
297+
return name
293298

294299

295300
MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds

tests/test_profiler.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
from sentry_sdk.tracing import Transaction
1717

1818

19-
minimum_python_33 = pytest.mark.skipif(
20-
sys.version_info < (3, 3), reason="Profiling is only supported in Python >= 3.3"
21-
)
19+
def requires_python_version(major, minor, reason=None):
20+
if reason is None:
21+
reason = "Requires Python{}.{}".format(major, minor)
22+
return pytest.mark.skipif(sys.version_info < (major, minor), reason=reason)
2223

2324

2425
def process_test_sample(sample):
2526
return [(tid, (stack, stack)) for tid, stack in sample]
2627

2728

28-
@minimum_python_33
29+
@requires_python_version(3, 3)
2930
def test_profiler_invalid_mode(teardown_profiling):
3031
with pytest.raises(ValueError):
3132
setup_profiler({"_experiments": {"profiler_mode": "magic"}})
@@ -126,7 +127,9 @@ def static_method():
126127
),
127128
pytest.param(
128129
GetFrame().instance_method_wrapped()(),
129-
"wrapped",
130+
"wrapped"
131+
if sys.version_info < (3, 11)
132+
else "GetFrame.instance_method_wrapped.<locals>.wrapped",
130133
id="instance_method_wrapped",
131134
),
132135
pytest.param(
@@ -136,14 +139,15 @@ def static_method():
136139
),
137140
pytest.param(
138141
GetFrame().class_method_wrapped()(),
139-
"wrapped",
142+
"wrapped"
143+
if sys.version_info < (3, 11)
144+
else "GetFrame.class_method_wrapped.<locals>.wrapped",
140145
id="class_method_wrapped",
141146
),
142147
pytest.param(
143148
GetFrame().static_method(),
144-
"GetFrame.static_method",
149+
"static_method" if sys.version_info < (3, 11) else "GetFrame.static_method",
145150
id="static_method",
146-
marks=pytest.mark.skip(reason="unsupported"),
147151
),
148152
pytest.param(
149153
GetFrame().inherited_instance_method(),
@@ -152,7 +156,9 @@ def static_method():
152156
),
153157
pytest.param(
154158
GetFrame().inherited_instance_method_wrapped()(),
155-
"wrapped",
159+
"wrapped"
160+
if sys.version_info < (3, 11)
161+
else "GetFrameBase.inherited_instance_method_wrapped.<locals>.wrapped",
156162
id="instance_method_wrapped",
157163
),
158164
pytest.param(
@@ -162,14 +168,17 @@ def static_method():
162168
),
163169
pytest.param(
164170
GetFrame().inherited_class_method_wrapped()(),
165-
"wrapped",
171+
"wrapped"
172+
if sys.version_info < (3, 11)
173+
else "GetFrameBase.inherited_class_method_wrapped.<locals>.wrapped",
166174
id="inherited_class_method_wrapped",
167175
),
168176
pytest.param(
169177
GetFrame().inherited_static_method(),
170-
"GetFrameBase.static_method",
178+
"inherited_static_method"
179+
if sys.version_info < (3, 11)
180+
else "GetFrameBase.inherited_static_method",
171181
id="inherited_static_method",
172-
marks=pytest.mark.skip(reason="unsupported"),
173182
),
174183
],
175184
)
@@ -255,7 +264,7 @@ def get_scheduler_threads(scheduler):
255264
return [thread for thread in threading.enumerate() if thread.name == scheduler.name]
256265

257266

258-
@minimum_python_33
267+
@requires_python_version(3, 3)
259268
@pytest.mark.parametrize(
260269
("scheduler_class",),
261270
[pytest.param(SleepScheduler, id="sleep scheduler")],

0 commit comments

Comments
 (0)