Skip to content

Commit 086e385

Browse files
authored
feat(profiling): Use co_qualname in python 3.11 (#1831)
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 0714d9f commit 086e385

File tree

3 files changed

+75
-58
lines changed

3 files changed

+75
-58
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: 51 additions & 46 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,
@@ -269,55 +269,60 @@ def extract_frame(frame, cwd):
269269
)
270270

271271

272-
def get_frame_name(frame):
273-
# type: (FrameType) -> str
272+
if PY311:
274273

275-
# in 3.11+, there is a frame.f_code.co_qualname that
276-
# we should consider using instead where possible
274+
def get_frame_name(frame):
275+
# type: (FrameType) -> str
276+
return frame.f_code.co_qualname # type: ignore
277277

278-
f_code = frame.f_code
279-
co_varnames = f_code.co_varnames
278+
else:
280279

281-
# co_name only contains the frame name. If the frame was a method,
282-
# the class name will NOT be included.
283-
name = f_code.co_name
280+
def get_frame_name(frame):
281+
# type: (FrameType) -> str
284282

285-
# if it was a method, we can get the class name by inspecting
286-
# the f_locals for the `self` argument
287-
try:
288-
if (
289-
# the co_varnames start with the frame's positional arguments
290-
# and we expect the first to be `self` if its an instance method
291-
co_varnames
292-
and co_varnames[0] == "self"
293-
and "self" in frame.f_locals
294-
):
295-
for cls in frame.f_locals["self"].__class__.__mro__:
296-
if name in cls.__dict__:
297-
return "{}.{}".format(cls.__name__, name)
298-
except AttributeError:
299-
pass
300-
301-
# if it was a class method, (decorated with `@classmethod`)
302-
# we can get the class name by inspecting the f_locals for the `cls` argument
303-
try:
304-
if (
305-
# the co_varnames start with the frame's positional arguments
306-
# and we expect the first to be `cls` if its a class method
307-
co_varnames
308-
and co_varnames[0] == "cls"
309-
and "cls" in frame.f_locals
310-
):
311-
for cls in frame.f_locals["cls"].__mro__:
312-
if name in cls.__dict__:
313-
return "{}.{}".format(cls.__name__, name)
314-
except AttributeError:
315-
pass
316-
317-
# nothing we can do if it is a staticmethod (decorated with @staticmethod)
318-
319-
# we've done all we can, time to give up and return what we have
320-
return name
283+
f_code = frame.f_code
284+
co_varnames = f_code.co_varnames
285+
286+
# co_name only contains the frame name. If the frame was a method,
287+
# the class name will NOT be included.
288+
name = f_code.co_name
289+
290+
# if it was a method, we can get the class name by inspecting
291+
# the f_locals for the `self` argument
292+
try:
293+
if (
294+
# the co_varnames start with the frame's positional arguments
295+
# and we expect the first to be `self` if its an instance method
296+
co_varnames
297+
and co_varnames[0] == "self"
298+
and "self" in frame.f_locals
299+
):
300+
for cls in frame.f_locals["self"].__class__.__mro__:
301+
if name in cls.__dict__:
302+
return "{}.{}".format(cls.__name__, name)
303+
except AttributeError:
304+
pass
305+
306+
# if it was a class method, (decorated with `@classmethod`)
307+
# we can get the class name by inspecting the f_locals for the `cls` argument
308+
try:
309+
if (
310+
# the co_varnames start with the frame's positional arguments
311+
# and we expect the first to be `cls` if its a class method
312+
co_varnames
313+
and co_varnames[0] == "cls"
314+
and "cls" in frame.f_locals
315+
):
316+
for cls in frame.f_locals["cls"].__mro__:
317+
if name in cls.__dict__:
318+
return "{}.{}".format(cls.__name__, name)
319+
except AttributeError:
320+
pass
321+
322+
# nothing we can do if it is a staticmethod (decorated with @staticmethod)
323+
324+
# we've done all we can, time to give up and return what we have
325+
return name
321326

322327

323328
MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds

tests/test_profiler.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
gevent = None
2323

2424

25-
minimum_python_33 = pytest.mark.skipif(
26-
sys.version_info < (3, 3), reason="Profiling is only supported in Python >= 3.3"
27-
)
25+
def requires_python_version(major, minor, reason=None):
26+
if reason is None:
27+
reason = "Requires Python {}.{}".format(major, minor)
28+
return pytest.mark.skipif(sys.version_info < (major, minor), reason=reason)
29+
2830

2931
requires_gevent = pytest.mark.skipif(gevent is None, reason="gevent not enabled")
3032

@@ -33,6 +35,7 @@ def process_test_sample(sample):
3335
return [(tid, (stack, stack)) for tid, stack in sample]
3436

3537

38+
@requires_python_version(3, 3)
3639
@pytest.mark.parametrize(
3740
"mode",
3841
[
@@ -146,7 +149,9 @@ def static_method():
146149
),
147150
pytest.param(
148151
GetFrame().instance_method_wrapped()(),
149-
"wrapped",
152+
"wrapped"
153+
if sys.version_info < (3, 11)
154+
else "GetFrame.instance_method_wrapped.<locals>.wrapped",
150155
id="instance_method_wrapped",
151156
),
152157
pytest.param(
@@ -156,14 +161,15 @@ def static_method():
156161
),
157162
pytest.param(
158163
GetFrame().class_method_wrapped()(),
159-
"wrapped",
164+
"wrapped"
165+
if sys.version_info < (3, 11)
166+
else "GetFrame.class_method_wrapped.<locals>.wrapped",
160167
id="class_method_wrapped",
161168
),
162169
pytest.param(
163170
GetFrame().static_method(),
164-
"GetFrame.static_method",
171+
"static_method" if sys.version_info < (3, 11) else "GetFrame.static_method",
165172
id="static_method",
166-
marks=pytest.mark.skip(reason="unsupported"),
167173
),
168174
pytest.param(
169175
GetFrame().inherited_instance_method(),
@@ -172,7 +178,9 @@ def static_method():
172178
),
173179
pytest.param(
174180
GetFrame().inherited_instance_method_wrapped()(),
175-
"wrapped",
181+
"wrapped"
182+
if sys.version_info < (3, 11)
183+
else "GetFrameBase.inherited_instance_method_wrapped.<locals>.wrapped",
176184
id="instance_method_wrapped",
177185
),
178186
pytest.param(
@@ -182,14 +190,17 @@ def static_method():
182190
),
183191
pytest.param(
184192
GetFrame().inherited_class_method_wrapped()(),
185-
"wrapped",
193+
"wrapped"
194+
if sys.version_info < (3, 11)
195+
else "GetFrameBase.inherited_class_method_wrapped.<locals>.wrapped",
186196
id="inherited_class_method_wrapped",
187197
),
188198
pytest.param(
189199
GetFrame().inherited_static_method(),
190-
"GetFrameBase.static_method",
200+
"inherited_static_method"
201+
if sys.version_info < (3, 11)
202+
else "GetFrameBase.inherited_static_method",
191203
id="inherited_static_method",
192-
marks=pytest.mark.skip(reason="unsupported"),
193204
),
194205
],
195206
)
@@ -275,7 +286,7 @@ def get_scheduler_threads(scheduler):
275286
return [thread for thread in threading.enumerate() if thread.name == scheduler.name]
276287

277288

278-
@minimum_python_33
289+
@requires_python_version(3, 3)
279290
@pytest.mark.parametrize(
280291
("scheduler_class",),
281292
[

0 commit comments

Comments
 (0)