Skip to content

Commit d1b0082

Browse files
committed
feat: Support for Python 3.12, 3.13 and 3.14
1 parent fccf708 commit d1b0082

File tree

11 files changed

+146
-29
lines changed

11 files changed

+146
-29
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
matrix:
1919
# Test all supported versions on Ubuntu:
2020
os: [ubuntu-latest]
21-
python: ["3.9", "3.10", "3.11", "pypy-3.10"]
21+
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.10"]
2222
experimental: [false]
2323
# include:
2424
# - os: macos-latest

setup.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,38 @@
11
from setuptools import setup, find_packages, Extension
2-
from distutils.command.build_py import build_py
2+
from setuptools.command.build_py import build_py
3+
from setuptools.command.build_ext import build_ext
34
import os, sys
45
import subprocess
5-
import platform
66

77
IS_PYPY = '__pypy__' in sys.builtin_module_names
88

9+
BASEDIR = os.path.dirname(os.path.abspath(__file__))
10+
911
class vmprof_build(build_py, object):
1012
def run(self):
1113
super(vmprof_build, self).run()
1214

13-
BASEDIR = os.path.dirname(os.path.abspath(__file__))
15+
class vmprof_build_ext(build_ext, object):
16+
"""build_ext that runs libbacktrace configure before building.
17+
This is needed because libbacktrace does not have a pre-built library for all platforms.
18+
"""
19+
def run(self):
20+
# configure libbacktrace on Unix systems (not Windows/macOS)
21+
if sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
22+
libbacktrace_dir = os.path.join(BASEDIR, "src", "libbacktrace")
23+
config_h = os.path.join(libbacktrace_dir, "config.h")
24+
# only run configure if config.h doesn't exist
25+
if not os.path.exists(config_h):
26+
orig_dir = os.getcwd()
27+
os.chdir(libbacktrace_dir)
28+
try:
29+
# generate configure script if it doesn't exist
30+
if not os.path.exists("configure"):
31+
subprocess.check_call(["autoreconf", "-i"])
32+
subprocess.check_call(["./configure"])
33+
finally:
34+
os.chdir(orig_dir)
35+
super(vmprof_build_ext, self).run()
1436

1537
def _supported_unix():
1638
if sys.platform.startswith('linux'):
@@ -65,20 +87,13 @@ def _supported_unix():
6587
'src/libbacktrace/posix.c',
6688
'src/libbacktrace/sort.c',
6789
]
68-
# configure libbacktrace!!
69-
class vmprof_build(build_py, object):
70-
def run(self):
71-
orig_dir = os.getcwd()
72-
os.chdir(os.path.join(BASEDIR, "src", "libbacktrace"))
73-
subprocess.check_call(["./configure"])
74-
os.chdir(orig_dir)
75-
super(vmprof_build, self).run()
7690

7791
else:
7892
raise NotImplementedError("platform '%s' is not supported!" % sys.platform)
79-
extra_compile_args.append('-I src/')
80-
extra_compile_args.append('-I src/libbacktrace')
81-
if sys.version_info[:2] == (3,11):
93+
# use absolute paths for include directories so compilation works from any directory
94+
extra_compile_args.append('-I' + os.path.join(BASEDIR, 'src'))
95+
extra_compile_args.append('-I' + os.path.join(BASEDIR, 'src', 'libbacktrace'))
96+
if sys.version_info[:2] >= (3,11):
8297
extra_source_files += ['src/populate_frames.c']
8398
ext_modules = [Extension('_vmprof',
8499
sources=[
@@ -116,14 +131,14 @@ def run(self):
116131
description="Python's vmprof client",
117132
long_description='See https://vmprof.readthedocs.org/',
118133
url='https://github.com/vmprof/vmprof-python',
119-
cmdclass={'build_py': vmprof_build},
134+
cmdclass={'build_py': vmprof_build, 'build_ext': vmprof_build_ext},
120135
install_requires=[
121136
'requests',
122137
'six',
123138
'pytz',
124139
'colorama',
125140
] + extra_install_requires,
126-
python_requires='<3.12',
141+
python_requires='<3.15',
127142
tests_require=['pytest','cffi','hypothesis'],
128143
entry_points = {
129144
'console_scripts': [
@@ -140,4 +155,4 @@ def run(self):
140155
zip_safe=False,
141156
include_package_data=True,
142157
ext_modules=ext_modules,
143-
)
158+
)

src/_vmprof.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
#ifndef RPYTHON_VMPROF
1616
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
17-
#include "internal/pycore_frame.h"
1817
#include "populate_frames.h"
1918
#endif
2019
#endif
@@ -140,7 +139,11 @@ void emit_all_code_objects(PyObject * seen_code_ids)
140139
Py_ssize_t i, size;
141140
void * param[2];
142141

142+
#if PY_VERSION_HEX >= 0x030D0000 /* >= 3.13 */
143+
gc_module = PyImport_ImportModule("gc");
144+
#else
143145
gc_module = PyImport_ImportModuleNoBlock("gc");
146+
#endif
144147
if (gc_module == NULL)
145148
goto error;
146149

src/populate_frames.c

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
// 0x030B0000 is 3.11.
99
#define PY_311 0x030B0000
10+
// 0x030D0000 is 3.13.
11+
#define PY_313 0x030D0000
12+
// 0x030E0000 is 3.14.
13+
#define PY_314 0x030E0000
14+
1015
#if PY_VERSION_HEX >= PY_311
1116

1217
/**
@@ -22,15 +27,26 @@
2227
*/
2328

2429
#define Py_BUILD_CORE
30+
#if PY_VERSION_HEX >= PY_314
31+
// Python 3.14 moved frame internals to pycore_interpframe.h
32+
#include "internal/pycore_interpframe.h"
33+
#else
2534
#include "internal/pycore_frame.h"
35+
#endif
2636
#undef Py_BUILD_CORE
2737

2838
// Modified from
2939
// https://github.com/python/cpython/blob/v3.11.4/Python/pystate.c#L1278-L1285
3040
_PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame(
3141
PyThreadState *tstate) {
3242
assert(tstate != NULL);
43+
#if PY_VERSION_HEX >= PY_313
44+
// In Python 3.13+, cframe was removed and current_frame is directly on tstate
45+
_PyInterpreterFrame *f = tstate->current_frame;
46+
#else
47+
// Python 3.11 and 3.12 use cframe->current_frame
3348
_PyInterpreterFrame *f = tstate->cframe->current_frame;
49+
#endif
3450
while (f && _PyFrame_IsIncomplete(f)) {
3551
f = f->previous;
3652
}
@@ -47,7 +63,13 @@ PyCodeObject *unsafe_PyInterpreterFrame_GetCode(
4763
_PyInterpreterFrame *frame) {
4864
assert(frame != NULL);
4965
assert(!_PyFrame_IsIncomplete(frame));
66+
#if PY_VERSION_HEX >= PY_313
67+
// In Python 3.13+, use the _PyFrame_GetCode inline function
68+
// f_code was renamed to f_executable
69+
PyCodeObject *code = _PyFrame_GetCode(frame);
70+
#else
5071
PyCodeObject *code = frame->f_code;
72+
#endif
5173
assert(code != NULL);
5274
return code;
5375
}
@@ -71,6 +93,10 @@ _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(
7193
// this function is not available in libpython
7294
int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) {
7395
int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT);
96+
#if PY_VERSION_HEX >= PY_313
97+
return PyCode_Addr2Line(_PyFrame_GetCode(frame), addr);
98+
#else
7499
return PyCode_Addr2Line(frame->f_code, addr);
100+
#endif
75101
}
76-
#endif // PY_VERSION_HEX >= PY_311
102+
#endif // PY_VERSION_HEX >= PY_311

src/populate_frames.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77

88
#include <frameobject.h>
99

10+
// 0x030E0000 is 3.14.
11+
#define PY_314 0x030E0000
12+
1013
#define Py_BUILD_CORE
14+
#if PY_VERSION_HEX >= PY_314
15+
// Python 3.14 moved frame internals to pycore_interpframe.h
16+
#include "internal/pycore_interpframe.h"
17+
#else
1118
#include "internal/pycore_frame.h"
19+
#endif
1220
#undef Py_BUILD_CORE
1321

1422
_PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame(PyThreadState *tstate);
@@ -19,4 +27,4 @@ _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(_PyInterpreterFrame *fram
1927

2028
int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame);
2129

22-
#endif
30+
#endif

src/vmp_stack.c

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,50 @@ PY_EVAL_RETURN_T * vmprof_eval(PY_STACK_FRAME_T *f, int throwflag) { return NULL
7474
static intptr_t *vmp_ranges = NULL;
7575
static ssize_t vmp_range_count = 0;
7676
static int vmp_native_traces_enabled = 0;
77+
78+
/**
79+
* Check if the given function is a Python eval frame function.
80+
*
81+
* On Python 3.13, the direct pointer comparison with _PyEval_EvalFrameDefault
82+
* may fail due to internal interpreter changes. This function provides a fallback
83+
* by checking the function name using libunwind.
84+
*
85+
* @param pip Pointer to the procedure info from libunwind
86+
* @param cursor Pointer to the libunwind cursor (for name lookup fallback)
87+
* @return 1 if this is an eval frame, 0 otherwise
88+
*/
89+
static int is_vmprof_eval_frame(unw_proc_info_t *pip, unw_cursor_t *cursor) {
90+
// First try fast pointer comparison (works on most Python versions)
91+
if (IS_VMPROF_EVAL((void*)pip->start_ip)) {
92+
return 1;
93+
}
94+
95+
#if PY_VERSION_HEX >= 0x030B0000 /* Python 3.11+ needs name-based fallback */
96+
// On Python 3.11+, the pointer comparison may fail due to interpreter changes.
97+
// Technically needed only on 3.13, yet safe to use on all 3.11+.
98+
// Fall back to checking the function name.
99+
char proc_name[128];
100+
unw_word_t offset;
101+
102+
if (unw_get_proc_name(cursor, proc_name, sizeof(proc_name), &offset) == 0) {
103+
// Check for known Python eval frame function names
104+
// _PyEval_EvalFrameDefault is the main eval function since Python 3.6
105+
if (strstr(proc_name, "_PyEval_EvalFrameDefault") != NULL) {
106+
return 1;
107+
}
108+
// PyEval_EvalCode is the entry point for code evaluation
109+
if (strstr(proc_name, "PyEval_EvalCode") != NULL) {
110+
return 1;
111+
}
112+
// Also check for potential variants or wrappers
113+
if (strstr(proc_name, "PyEval_EvalFrame") != NULL) {
114+
return 1;
115+
}
116+
}
117+
#endif
118+
119+
return 0;
120+
}
77121
#endif
78122
static int _vmp_profiles_lines = 0;
79123

@@ -338,7 +382,7 @@ int vmp_walk_and_record_stack(_PyInterpreterFrame *frame, void ** result,
338382
}
339383
#endif
340384

341-
if (IS_VMPROF_EVAL((void*)pip.start_ip)) {
385+
if (is_vmprof_eval_frame(&pip, &cursor)) {
342386
// yes we found one stack entry of the python frames!
343387
return vmp_walk_and_record_python_stack_only(frame, result, max_depth, depth, pc);
344388
#ifdef PYPY_JIT_CODEMAP
@@ -492,7 +536,7 @@ int vmp_walk_and_record_stack(PY_STACK_FRAME_T *frame, void ** result,
492536
}
493537
#endif
494538

495-
if (IS_VMPROF_EVAL((void*)pip.start_ip)) {
539+
if (is_vmprof_eval_frame(&pip, &cursor)) {
496540
// yes we found one stack entry of the python frames!
497541
return vmp_walk_and_record_python_stack_only(frame, result, max_depth, depth, pc);
498542
#ifdef PYPY_JIT_CODEMAP

src/vmp_stack.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
#ifndef RPYTHON_VMPROF
66
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
7+
#define Py_BUILD_CORE
8+
#if PY_VERSION_HEX >= 0x030E0000 /* >= 3.14 */
9+
#include "internal/pycore_interpframe.h"
10+
#else
711
#include "internal/pycore_frame.h"
12+
#endif
13+
#undef Py_BUILD_CORE
814
#include "populate_frames.h"
915
#endif
1016
#endif

src/vmprof_win.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
#ifndef RPYTHON_VMPROF
44
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
5-
#include "internal/pycore_frame.h"
65
#include "populate_frames.h"
76
#endif
87
#endif
98

109
volatile int thread_started = 0;
1110
volatile int enabled = 0;
11+
#ifndef RPYTHON_VMPROF
12+
static PY_WIN_THREAD_STATE *target_tstate = NULL;
13+
#endif
1214

1315
HANDLE write_mutex;
1416

@@ -174,6 +176,8 @@ long __stdcall vmprof_mainloop(void *arg)
174176
continue;
175177
}
176178
tstate = get_current_thread_state();
179+
if (!tstate)
180+
tstate = target_tstate;
177181
if (!tstate)
178182
continue;
179183
depth = vmprof_snapshot_thread(tstate->thread_id, tstate, stack);
@@ -221,6 +225,9 @@ int vmprof_enable(int memory, int native, int real_time)
221225
thread_started = 1;
222226
}
223227
enabled = 1;
228+
#ifndef RPYTHON_VMPROF
229+
target_tstate = PyThreadState_Get();
230+
#endif
224231
return 0;
225232
}
226233

@@ -231,6 +238,9 @@ int vmprof_disable(void)
231238
(void)vmp_write_time_now(MARKER_TRAILER);
232239

233240
enabled = 0;
241+
#ifndef RPYTHON_VMPROF
242+
target_tstate = NULL;
243+
#endif
234244
vmp_set_profile_fileno(-1);
235245
return 0;
236246
}

vmprof/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class IniParser(object):
118118

119119
def __init__(self, f):
120120
self.ini_parser = configparser.ConfigParser()
121-
self.ini_parser.readfp(f)
121+
self.ini_parser.read_file(f)
122122

123123
def get_option(self, name, type, default=None):
124124
if type == float:

vmprof/test/test_c_source.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def setup_class(cls):
3131
libs.append('unwind-x86_64')
3232
# trick: compile with _CFFI_USE_EMBEDDING=1 which will not define Py_LIMITED_API
3333
sources = []
34-
if sys.version_info[:2] == (3,11):
35-
sources += ['src/populate_frames.c']# needed for cp311 but must not be included in py < 3.11
34+
if sys.version_info[:2] >= (3, 11):
35+
sources += ['src/populate_frames.c']# needed for py 3.11+ but must not be included in py < 3.11
3636
stack_ffi.set_source("vmprof.test._test_stack", source, include_dirs=['src'],
3737
define_macros=[('_CFFI_USE_EMBEDDING',1), ('PY_TEST',1),
3838
('VMP_SUPPORTS_NATIVE_PROFILING',1)],

0 commit comments

Comments
 (0)