diff --git a/Include/internal/pycore_frame.h b/Include/internal/pycore_frame.h index 506c20ca1950bd..d5115adf32ec1f 100644 --- a/Include/internal/pycore_frame.h +++ b/Include/internal/pycore_frame.h @@ -27,6 +27,10 @@ struct _frame { char f_trace_lines; /* Emit per-line trace events? */ char f_trace_opcodes; /* Emit per-opcode trace events? */ PyObject *f_extra_locals; /* Dict for locals set by users using f_locals, could be NULL */ + /* This is purely for backwards compatibility for PyEval_GetLocals. + PyEval_GetLocals requires a borrowed reference so the actual reference + is stored here */ + PyObject *f_locals_cache; /* The frame data, if this frame object owns the frame */ PyObject *_f_frame_data[1]; }; diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 69dccf9a9322f6..81789b20433be9 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1603,7 +1603,7 @@ class C(object): pass def func(): return sys._getframe() x = func() - check(x, size('3Pi2cP7P2ic??2P')) + check(x, size('3Pi2c2P7P2ic??2P')) # function def func(): pass check(func, size('16Pi')) diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-05-30-04-11-36.gh-issue-118934.fbDqve.rst b/Misc/NEWS.d/next/Core and Builtins/2024-05-30-04-11-36.gh-issue-118934.fbDqve.rst new file mode 100644 index 00000000000000..3087034fe458b8 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2024-05-30-04-11-36.gh-issue-118934.fbDqve.rst @@ -0,0 +1 @@ +Make ``PyEval_GetLocals`` return borrowed reference diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 3dc9ff058a5c9e..2ef3beaf83a48f 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -1627,6 +1627,7 @@ frame_dealloc(PyFrameObject *f) Py_CLEAR(f->f_back); Py_CLEAR(f->f_trace); Py_CLEAR(f->f_extra_locals); + Py_CLEAR(f->f_locals_cache); PyObject_GC_Del(f); Py_XDECREF(co); Py_TRASHCAN_END; @@ -1638,6 +1639,7 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg) Py_VISIT(f->f_back); Py_VISIT(f->f_trace); Py_VISIT(f->f_extra_locals); + Py_VISIT(f->f_locals_cache); if (f->f_frame->owner != FRAME_OWNED_BY_FRAME_OBJECT) { return 0; } @@ -1650,6 +1652,7 @@ frame_tp_clear(PyFrameObject *f) { Py_CLEAR(f->f_trace); Py_CLEAR(f->f_extra_locals); + Py_CLEAR(f->f_locals_cache); /* locals and stack */ _PyStackRef *locals = _PyFrame_GetLocalsArray(f->f_frame); @@ -1787,6 +1790,7 @@ _PyFrame_New_NoTrack(PyCodeObject *code) f->f_trace_opcodes = 0; f->f_lineno = 0; f->f_extra_locals = NULL; + f->f_locals_cache = NULL; return f; } diff --git a/Python/ceval.c b/Python/ceval.c index d8bc830f8e80c1..026e018676caad 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -2525,6 +2525,7 @@ _PyEval_GetBuiltinId(_Py_Identifier *name) PyObject * PyEval_GetLocals(void) { + // We need to return a borrowed reference here, so some tricks are needed PyThreadState *tstate = _PyThreadState_GET(); _PyInterpreterFrame *current_frame = _PyThreadState_GetFrame(tstate); if (current_frame == NULL) { @@ -2532,7 +2533,37 @@ PyEval_GetLocals(void) return NULL; } - PyObject *locals = _PyEval_GetFrameLocals(); + // Be aware that this returns a new reference + PyObject *locals = _PyFrame_GetLocals(current_frame); + + if (locals == NULL) { + return NULL; + } + + if (PyFrameLocalsProxy_Check(locals)) { + PyFrameObject *f = _PyFrame_GetFrameObject(current_frame); + PyObject *ret = f->f_locals_cache; + if (ret == NULL) { + PyObject *ret = PyDict_New(); + if (ret == NULL) { + Py_DECREF(locals); + return NULL; + } + f->f_locals_cache = ret; + } + if (PyDict_Update(ret, locals) < 0) { + // At this point, if the cache dict is broken, it will stay broken, as + // trying to clean it up or replace it will just cause other problems + ret = NULL; + } + Py_DECREF(locals); + return ret; + } + + assert(PyMapping_Check(locals)); + assert(Py_REFCNT(locals) > 1); + Py_DECREF(locals); + return locals; }