8000 [PEP 558 - WIP] bpo-30744: Trace hooks no longer reset closure state by ncoghlan · Pull Request #3640 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

[PEP 558 - WIP] bpo-30744: Trace hooks no longer reset closure state #3640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
7626a0e
bpo-30744: Trace hooks no longer reset closure state
ncoghlan Sep 18, 2017
3cbb73c
Disable the current broken writeback logic
ncoghlan Nov 5, 2017
01f3f34
Failing test case for writeback functionality
ncoghlan Nov 5, 2017
4f6dd93
Initial skeleton for a write-through proxy
ncoghlan Nov 5, 2017
cd45da7
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Apr 21, 2019
acbf587
Finish adding the write-through proxy
ncoghlan Apr 21, 2019
d1a9014
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan May 21, 2019
5ea8bcf
Add test case for the PEP 558 locals() behaviour
ncoghlan May 22, 2019
fe92891
Actually implement most of the PEP and fix the tests
ncoghlan May 22, 2019
ac9e0bf
Fix segfault on cleared frames
ncoghlan May 27, 2019
6774e71
Use correct printf formatting code
ncoghlan May 27, 2019
74c51e4
Initial skeleton for other mutable mapping methods
ncoghlan May 28, 2019
0e5fbf3
Break ref cycle when frame finishes executing
ncoghl 8000 an May 30, 2019
9e3ce53
Remove implicit frame locals update
ncoghlan May 30, 2019
9479557
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan May 30, 2019
8e886ef
Avoid double DECREF on error
ncoghlan May 30, 2019
5b63e7c
Attempt to make dealloc more robust under gc
ncoghlan May 30, 2019
f0ecee3
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Dec 29, 2019
1752b54
Fix post-merge compilation errors
ncoghlan Dec 29, 2019
348a56d
Implement flp.pop()
ncoghlan Dec 29, 2019
93f512c
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Dec 29, 2019
39ec4d8
Refactor ref map creation
ncoghlan Dec 29, 2019
7078632
Correctly manage fast local refcounts
ncoghlan Dec 29, 2019
ed5f86e
Allow closure updates after frame termination
ncoghlan Dec 29, 2019
a216747
Refactor frame post-eval cleanup
ncoghlan Dec 29, 2019
a0dc787
Use full word in API name
ncoghlan Dec 29, 2019
7b02bed
Update to match latest PEP draft
ncoghlan Dec 29, 2019
e9876b5
Update test_scope for snapshot semantics
ncoghlan Dec 29, 2019
0033c60
Update test_sys_settrace for snapshot semantics
ncoghlan Dec 29, 2019
a5a8b19
Fix pop/delete locals proxy bug
ncoghlan Dec 29, 2019
6c98f48
Update argument clinic output
ncoghlan Dec 30, 2019
617f6ab
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Dec 30, 2019
729f121
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Jan 18, 2020
619fb75
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Feb 2, 2020
1fe964e
Migrate to revised public API design
ncoghlan Feb 2, 2020
b047ae4
Rename PyFrame_LocalsIsSnapshot to PyFrame_GetLocalsReturnsSnapshot
ncoghlan Feb 2, 2020
d1a8420
Mark fast locals proxy as an internal type
ncoghlan Feb 2, 2020
82108ff
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Feb 15, 2020
eccb1ea
Update draft C API to match latest PEP text
ncoghlan Feb 16, 2020
c1933e7
Migrate exec() and eval() to PyLocals_Get()
ncoghlan Feb 16, 2020
161ad47
Merge remote-tracking branch 'origin/master' into bpo-30744-make-loca…
ncoghlan Feb 22, 2020
68f10ce
Avoid circular reference between locals proxy and frame
ncoghlan Feb 22, 2020
29ce344
Add back implicit view refresh in Python trace hook
ncoghlan Feb 22, 2020
ed6e53b
Attempt to tidy up Mac OS X compile warnings/errors
ncoghlan Feb 22, 2020
96c77cb
Merge remote-tracking branch 'origin/main' into bpo-30744-make-locals…
ncoghlan Jul 3, 2021
a348d08
Fix Argument Clinic checksum
ncoghlan Jul 3, 2021
69c8f19
Fix stable ABI minimum version
ncoghlan Jul 3, 2021
caeaf66
Bring implementation into line with latest PEP version
ncoghlan Jul 10, 2021
7ec5d26
Register new stable ABI additions
ncoghlan Jul 10, 2021
7ddc3eb
Add FLP str(), fix various value lookup issues
ncoghlan Jul 10, 2021
7400a46
Uninitialised fields will get you every time
ncoghlan Jul 10, 2021
5eae0d5
Fix refcounting, bdb segfault, pdb functionality
ncoghlan Jul 10, 2021
40db4e7
Delegate more operations to the full dynamic snapshot
ncoghlan Jul 10, 2021
74b97a3
Add TODO item for false positives in containment checks
ncoghlan Jul 10, 2021
9f16513
Only ensure frame snapshot is up to date in O(n) proxy operations
ncoghlan Jul 10, 2021
c477e24
Keep locals snapshot up to date when reading/writing individual keys
ncoghlan Jul 11, 2021
dd94608
Avoid false positives in FLP contains method
ncoghlan Jul 11, 2021
b7eb662
Merge remote-tracking branch 'origin/main' into bpo-30744-make-locals…
ncoghlan Jul 17, 2021
1484c10
Finish public C API, start dict API tests
ncoghlan Jul 17, 2021
3891c13
Merge remote-tracking branch 'origin/main' into bpo-30744-make-locals…
ncoghlan Jul 17, 2021
760ffa9
Remove debugging print statement
ncoghlan Jul 17, 2021
ae6b013
Regenerated stable ABI files
ncoghlan Jul 18, 2021
b03309b
Rename _PyLocals_Kind APIs to avoid potential confusion
ncoghlan Jul 21, 2021
cde5035
Merge remote-tracking branch 'origin/main' into bpo-30744-make-locals…
ncoghlan Jul 21, 2021
66d058c
PyLocals_GetReturnsCopy -> PyLocals_GetKind()
ncoghlan Jul 21, 2021
67c3958
Share fast_refs mapping between proxy objects
ncoghlan Aug 21, 2021
034345f
Remove debugging print
ncoghlan Aug 21, 2021
3c49ff8
Defer value cache refresh until needed, start fleshing out dict API t…
ncoghlan Aug 21, 2021
ea5f943
Add dict union operations to proxy
ncoghlan Aug 21, 2021
fcf99ca
Implement and test locals proxy clear() method
ncoghlan Aug 21, 2021
16e0581
Remove pointless print() call
ncoghlan Aug 21, 2021
c356949
Implement proxy pop() tests
ncoghlan Aug 21, 2021
8a4e788
Implement and test proxy popitem()
ncoghlan Aug 21, 2021
706eec4
Test popitem with cells and extra variables
ncoghlan Aug 21, 2021
35a017c
Implement and test setdefault()
ncoghlan Aug 21, 2021
06c406c
Implement and test proxy __sizeof__()
ncoghlan Aug 21, 2021
31493b9
Add C API test for the LocalsToFast exception
ncoghlan Aug 21, 2021
b587a41
Force enum size
ncoghlan Aug 23, 2021
e1b505d
Clarify code comment
ncoghlan Aug 23, 2021
2b27389
Keep track of defined names even on cleared frames
ncoghlan Aug 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Finish public C API, start dict API tests
  • Loading branch information
ncoghlan committed Jul 17, 2021
commit 1484c100ef9f5669f9e83b58c06a2dce4bfdec37
9 changes: 0 additions & 9 deletions Include/cpython/frameobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,13 @@ PyAPI_DATA(PyTypeObject) _PyFastLocalsProxy_Type;
// TODO: Add specific test cases for these (as any PyLocals_* tests won't cover
// checking the status of a frame other than the currently active one)
PyAPI_FUNC(PyObject *) PyFrame_GetLocals(PyFrameObject *);

// TODO: Implement the rest of these, and add API tests
PyAPI_FUNC(PyObject *) PyFrame_GetLocalsCopy(PyFrameObject *);
PyAPI_FUNC(PyObject *) PyFrame_GetLocalsView(PyFrameObject *);
PyAPI_FUNC(int) PyFrame_GetLocalsReturnsCopy(PyFrameObject *);

// Underlying API supporting PyEval_GetLocals()
PyAPI_FUNC(PyObject *) _PyFrame_BorrowLocals(PyFrameObject *);

/* Force an update of any selectively updated views previously returned by
* PyFrame_GetLocalsView(frame). Currently also needed in CPython when
* accessing the f_locals attribute directly and it is not a plain dict
* instance (otherwise it may report stale information).
*/
PyAPI_FUNC(int) PyFrame_RefreshLocalsView(PyFrameObject *);

#ifdef __cplusplus
}
#endif
144 changes: 144 additions & 0 deletions Lib/test/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import weakref

from test import support
from test.support import import_helper


class ClearTest(unittest.TestCase):
Expand Down Expand Up @@ -195,6 +196,149 @@ def test_f_lineno_del_segfault(self):
with self.assertRaises(AttributeError):
del f.f_lineno

class FastLocalsProxyTest(unittest.TestCase):

def check_proxy_contents(self, proxy, expected_contents):
# These checks should never implicitly resync the frame proxy's cache,
# even if the proxy is referenced as a local variable in the frame
self.assertEqual(len(proxy), len(expected_contents))
self.assertCountEqual(proxy, expected_contents)
self.assertCountEqual(proxy.keys(), expected_contents.keys())
self.assertCountEqual(proxy.values(), expected_contents.values())
self.assertCountEqual(proxy.items(), expected_contents.items())

def test_dict_operations(self):
# No real iteration order guarantees for the locals proxy, as it
# depends on exactly how the compiler composes the frame locals array
proxy = sys._getframe().f_locals
# Not yet set values aren't visible in the proxy
self.check_proxy_contents(proxy, {"self": self})

# Ensuring copying the proxy produces a plain dict instance
dict_copy = proxy.copy()
self.assertIsInstance(dict_copy, dict)
self.assertEqual(dict_copy.keys(), {"proxy", "self"})
# The proxy automatically updates its cache for O(n) operations like copying,
# but won't pick up new local variables until it is resync'ed with the frame
# or that particular key is accessed or queried
self.check_proxy_contents(proxy, dict_copy)
self.assertIn("dict_copy", proxy) # Implicitly updates cache for this key
dict_copy["dict_copy"] = dict_copy
self.check_proxy_contents(proxy, dict_copy)

self.fail("Test not finished yet")

def test_active_frame_c_apis(self):
# Use ctypes to access the C APIs under test
ctypes = import_helper.import_module('ctypes')
Py_IncRef = ctypes.pythonapi.Py_IncRef
PyEval_GetLocals = ctypes.pythonapi.PyEval_GetLocals
PyLocals_Get = ctypes.pythonapi.PyLocals_Get
PyLocals_GetReturnsCopy = ctypes.pythonapi.PyLocals_GetReturnsCopy
PyLocals_GetCopy = ctypes.pythonapi.PyLocals_GetCopy
PyLocals_GetView = ctypes.pythonapi.PyLocals_GetView
for capi_func in (Py_IncRef,):
capi_func.argtypes = (ctypes.py_object,)
for capi_func in (PyEval_GetLocals,
PyLocals_Get, PyLocals_GetCopy, PyLocals_GetView):
capi_func.restype = ctypes.py_object

# PyEval_GetLocals() always accesses the running frame,
# so Py_IncRef has to be called inline (no helper function)

# This test covers the retrieval APIs, the behavioural tests are covered
# elsewhere using the `frame.f_locals` attribute and the locals() builtin

# Test retrieval API behaviour in an optimised scope
print("Retrieving C locals cache for frame")
c_locals_cache = PyEval_GetLocals()
Py_IncRef(c_locals_cache) # Make the borrowed reference a real one
Py_IncRef(c_locals_cache) # Account for next check's borrowed reference
self.assertIs(PyEval_GetLocals(), c_locals_cache)
self.assertTrue(PyLocals_GetReturnsCopy())
locals_get = PyLocals_Get()
self.assertIsInstance(locals_get, dict)
self.assertIsNot(locals_get, c_locals_cache)
locals_copy = PyLocals_GetCopy()
self.assertIsInstance(locals_copy, dict)
self.assertIsNot(locals_copy, c_locals_cache)
locals_view = PyLocals_GetView()
self.assertIsInstance(locals_view, types.MappingProxyType)

# Test API behaviour in an unoptimised scope
class ExecFrame:
c_locals_cache = PyEval_GetLocals()
Py_IncRef(c_locals_cache) # Make the borrowed reference a real one
Py_IncRef(c_locals_cache) # Account for next check's borrowed reference
self.assertIs(PyEval_GetLocals(), c_locals_cache)
self.assertFalse(PyLocals_GetReturnsCopy())
locals_get = PyLocals_Get()
self.assertIs(locals_get, c_locals_cache)
locals_copy = PyLocals_GetCopy()
self.assertIsInstance(locals_copy, dict)
self.assertIsNot(locals_copy, c_locals_cache)
locals_view = PyLocals_GetView()
self.assertIsInstance(locals_view, types.MappingProxyType)

def test_arbitrary_frame_c_apis(self):
# Use ctypes to access the C APIs under test
ctypes = import_helper.import_module('ctypes')
Py_IncRef = ctypes.pythonapi.Py_IncRef
_PyFrame_BorrowLocals = ctypes.pythonapi._PyFrame_BorrowLocals
PyFrame_GetLocals = ctypes.pythonapi.PyFrame_GetLocals
PyFrame_GetLocalsReturnsCopy = ctypes.pythonapi.PyFrame_GetLocalsReturnsCopy
PyFrame_GetLocalsCopy = ctypes.pythonapi.PyFrame_GetLocalsCopy
PyFrame_GetLocalsView = ctypes.pythonapi.PyFrame_GetLocalsView
for capi_func in (Py_IncRef, _PyFrame_BorrowLocals,
PyFrame_GetLocals, PyFrame_GetLocalsReturnsCopy,
PyFrame_GetLocalsCopy, PyFrame_GetLocalsView):
capi_func.argtypes = (ctypes.py_object,)
for capi_func in (_PyFrame_BorrowLocals, PyFrame_GetLocals,
PyFrame_GetLocalsCopy, PyFrame_GetLocalsView):
capi_func.restype = ctypes.py_object

def get_c_locals(frame):
c_locals = _PyFrame_BorrowLocals(frame)
Py_IncRef(c_locals) # Make the borrowed reference a real one
return c_locals

# This test covers the retrieval APIs, the behavioural tests are covered
# elsewhere using the `frame.f_locals` attribute and the locals() builtin

# Test querying an optimised frame from an unoptimised scope
func_frame = sys._getframe()
cls_frame = None
def set_cls_frame(f):
nonlocal cls_frame
cls_frame = 6D40 f
class ExecFrame:
c_locals_cache = get_c_locals(func_frame)
self.assertIs(get_c_locals(func_frame), c_locals_cache)
self.assertTrue(PyFrame_GetLocalsReturnsCopy(func_frame))
locals_get = PyFrame_GetLocals(func_frame)
self.assertIsInstance(locals_get, dict)
self.assertIsNot(locals_get, c_locals_cache)
locals_copy = PyFrame_GetLocalsCopy(func_frame)
self.assertIsInstance(locals_copy, dict)
self.assertIsNot(locals_copy, c_locals_cache)
locals_view = PyFrame_GetLocalsView(func_frame)
self.assertIsInstance(locals_view, types.MappingProxyType)

# Keep the class frame alive for the functions below to access
set_cls_frame(sys._getframe())

# Test querying an unoptimised frame from an optimised scope
c_locals_cache = get_c_locals(cls_frame)
self.assertIs(get_c_locals(cls_frame), c_locals_cache)
self.assertFalse(PyFrame_GetLocalsReturnsCopy(cls_frame))
locals_get = PyFrame_GetLocals(cls_frame)
self.assertIs(locals_get, c_locals_cache)
locals_copy = PyFrame_GetLocalsCopy(cls_frame)
self.assertIsInstance(locals_copy, dict)
self.assertIsNot(locals_copy, c_locals_cache)
locals_view = PyFrame_GetLocalsView(cls_frame)
self.assertIsInstance(locals_view, types.MappingProxyType)


class ReprTest(unittest.TestCase):
"""
Expand Down
54 changes: 48 additions & 6 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ _frame_get_locals_mapping(PyFrameObject *f)
{
PyObject *locals = _PyFrame_Specials(f)[FRAME_SPECIALS_LOCALS_OFFSET];
if (locals == NULL) {
printf("Allocating new frame locals cache\n");
locals = _PyFrame_Specials(f)[FRAME_SPECIALS_LOCALS_OFFSET] = PyDict_New();
}
return locals;
Expand All @@ -53,16 +54,20 @@ _frame_get_updated_locals(PyFrameObject *f)
PyObject *
_PyFrame_BorrowLocals(PyFrameObject *f)
{
// This is called by PyEval_GetLocals(), which has historically returned
// a borrowed reference, so this does the same
// This frame API supports the PyEval_GetLocals() stable API, which has
// historically returned a borrowed reference (so this does the same)
return _frame_get_updated_locals(f);
}

PyObject *
PyFrame_GetLocals(PyFrameObject *f)
{
// This API implements the Python level locals() builtin
// This frame API implements the Python level locals() builtin
// and supports the PyLocals_Get() stable API
PyObject *updated_locals = _frame_get_updated_locals(f);
if (updated_locals == NULL) {
return NULL;
}
PyCodeObject *co = _PyFrame_GetCode(f);

assert(co);
Expand All @@ -77,6 +82,26 @@ PyFrame_GetLocals(PyFrameObject *f)
return updated_locals;
}

int
PyFrame_GetLocalsReturnsCopy(PyFrameObject *f)
{
// This frame API supports the stable PyLocals_GetReturnsCopy() API
PyCodeObject *co = _PyFrame_GetCode(f);
assert(co);
return (co->co_flags & CO_OPTIMIZED);
}

PyObject *
PyFrame_GetLocalsCopy(PyFrameObject *f)
{
// This frame API supports the stable PyLocals_GetCopy() API
PyObject *updated_locals = _frame_get_updated_locals(f);
if (updated_locals == NULL) {
return NULL;
}
return PyDict_Copy(updated_locals);
}

static PyObject *
frame_getlocals(PyFrameObject *f, void *Py_UNUSED(ignored))
{
Expand All @@ -97,11 +122,25 @@ frame_getlocals(PyFrameObject *f, void *Py_UNUSED(ignored))
} else {
// Share a direct locals reference for class and module scopes
f_locals_attr = _frame_get_updated_locals(f);
if (f_locals_attr == NULL) {
return NULL;
}
Py_INCREF(f_locals_attr);
}
return f_locals_attr;
}

PyObject *
PyFrame_GetLocalsView(PyFrameObject *f)
{
// This frame API supports the stable PyLocals_GetView() API
PyObject *rw_locals = frame_getlocals(f, NULL);
if (rw_locals == NULL) {
return NULL;
}
return PyDictProxy_New(rw_locals);
}

int
PyFrame_GetLineNumber(PyFrameObject *f)
{
Expand Down Expand Up @@ -1018,6 +1057,7 @@ PyFrame_FastToLocalsWithError(PyFrameObject *f)
return -1;
}
co = _PyFrame_GetCode(f);
assert(co);
fast = f->f_localsptr;
for (int i = 0; i < co->co_nlocalsplus; i++) {
_PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
Expand Down Expand Up @@ -1648,8 +1688,10 @@ static PyObject *
fastlocalsproxy_richcompare(fastlocalsproxyobject *flp, PyObject *w, int op)
{
// Need values, so use the dynamic snapshot on the frame
// Ensure it is up to date, as checking is O(n) anyway
PyObject *locals = _frame_get_updated_locals(flp->frame);
// Assume f_locals snapshot is up to date, as even though the worst
// case comparison is O(n) to determine equality, there are O(1) shortcuts
// for inequality checks (i.e. different sizes)
PyObject *locals = _frame_get_locals_mapping(flp->frame);
return PyObject_RichCompare(locals, w, op);
}

Expand Down Expand Up @@ -1841,7 +1883,7 @@ static PyObject *
fastlocalsproxy_str(fastlocalsproxyobject *flp)
{
// Need values, so use the dynamic snapshot on the frame
// Ensure it is up to date, as checking is O(n) anyway
// Ensure it is up to date, as displaying everything is O(n) anyway
PyObject *locals = _frame_get_updated_locals(flp->frame);
return PyObject_Str(locals);
}
Expand Down
40 changes: 40 additions & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -5798,6 +5798,46 @@ PyLocals_Get(void)
return PyFrame_GetLocals(current_frame);
}

int
PyLocals_GetReturnsCopy(void)
{
PyThreadState *tstate = _PyThreadState_GET();
PyFrameObject *current_frame = tstate->frame;
if (current_frame == NULL) {
_PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
return NULL;
}

return PyFrame_GetLocalsReturnsCopy(current_frame);
}

PyObject *
PyLocals_GetCopy(void)
{
PyThreadState *tstate = _PyThreadState_GET();
PyFrameObject *current_frame = tstate->frame;
if (current_frame == NULL) {
_PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
return NULL;
}

return PyFrame_GetLocalsCopy(current_frame);
}

PyObject *
PyLocals_GetView(void)
{
PyThreadState *tstate = _PyThreadState_GET();
PyFrameObject *current_frame = tstate->frame;
if (current_frame == NULL) {
_PyErr_SetString(tstate, PyExc_SystemError, "frame does not exist");
return NULL;
}

return PyFrame_GetLocalsView(current_frame);
}


PyObject *
PyEval_GetGlobals(void)
{
Expand Down
0