Skip to content

Commit 259c321

Browse files
Give class-closure cells private names so they don't collide with freevars
Store the class unit's synthetic cells as __classcell__/__classdictcell__ instead of __class__/__classdict__, and map method freevars of the public names to those private cells in LookupArg. This keeps an enclosing free named __class__ distinct from the class cell in localsplus. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 224b4b0 commit 259c321

3 files changed

Lines changed: 150 additions & 12 deletions

File tree

‎Lib/test/test_super.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pickle
55
import textwrap
66
import threading
7+
import types
78
import unittest
89
from unittest.mock import patch
910
from test.support import import_helper, threading_helper
@@ -204,6 +205,63 @@ def f():
204205

205206
self.assertIs(test_class, A)
206207

208+
def test_class_closure_uses_private_cellvar_name(self):
209+
# The class unit's cell must not share the localsplus name __class__
210+
# with a FREE load of an enclosing __class__.
211+
def outer(__class__):
212+
class C:
213+
enclosing = __class__
214+
def method(self):
215+
return __class__
216+
return C
217+
C = outer(object)
218+
self.assertIs(C.enclosing, object)
219+
self.assertIs(C().method(), C)
220+
221+
code = compile(
222+
"def outer(__class__):\n"
223+
" class C:\n"
224+
" enclosing = __class__\n"
225+
" def method(self):\n"
226+
" return __class__\n",
227+
"<test>", "exec")
228+
outer_code = next(c for c in code.co_consts
229+
if isinstance(c, types.CodeType) and c.co_name == "outer")
230+
class_code = next(c for c in outer_code.co_consts
231+
if isinstance(c, types.CodeType) and c.co_name == "C")
232+
self.assertIn("__classcell__", class_code.co_cellvars)
233+
self.assertNotIn("__class__", class_code.co_cellvars)
234+
self.assertIn("__class__", class_code.co_freevars)
235+
localsplus = (class_code.co_varnames + class_code.co_cellvars
236+
+ class_code.co_freevars)
237+
self.assertEqual(len(set(localsplus)), len(localsplus))
238+
239+
def test_class_cell_distinct_from_inlined_comp_cell(self):
240+
# Private __classcell__ coexists with an inlined-comp cell named
241+
# __class__; methods close over the private cell.
242+
ns = {}
243+
exec(
244+
"class C:\n"
245+
" def method(self):\n"
246+
" return __class__\n"
247+
" lambdas = [lambda: __class__ for __class__ in (1, 2)]\n",
248+
ns,
249+
)
250+
C = ns["C"]
251+
self.assertIs(C().method(), C)
252+
self.assertEqual([f() for f in C.lambdas], [2, 2])
253+
self.assertIsNot(C.lambdas[0].__closure__[0], C.method.__closure__[0])
254+
255+
class_code = next(c for c in compile(
256+
"class C:\n"
257+
" def method(self):\n"
258+
" return __class__\n"
259+
" lambdas = [lambda: __class__ for __class__ in (1, 2)]\n",
260+
"<test>", "exec").co_consts
261+
if isinstance(c, types.CodeType) and c.co_name == "C")
262+
self.assertIn("__classcell__", class_code.co_cellvars)
263+
self.assertIn("__class__", class_code.co_cellvars)
264+
207265
def test___classcell___expected_behaviour(self):
208266
# See issue #23722
209267
class Meta(type):

‎Python/codegen.c‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,10 +1641,9 @@ codegen_class_body(compiler *c, stmt_ty s, int firstlineno)
16411641
if (SYMTABLE_ENTRY(c)->ste_needs_classdict) {
16421642
ADDOP_IN_SCOPE(c, loc, LOAD_LOCALS);
16431643

1644-
// We can't use codegen_nameop here because we need to generate a
1645-
// STORE_DEREF in a class namespace, and codegen_nameop() won't do
1646-
// that by default.
1647-
ADDOP_N_IN_SCOPE(c, loc, STORE_DEREF, &_Py_ID(__classdict__), cellvars);
1644+
// Private cellvar __classdictcell__; codegen_nameop would not
1645+
// emit STORE_DEREF for a class-namespace nameop.
1646+
ADDOP_N_IN_SCOPE(c, loc, STORE_DEREF, &_Py_ID(__classdictcell__), cellvars);
16481647
}
16491648
if (SYMTABLE_ENTRY(c)->ste_has_conditional_annotations) {
16501649
ADDOP_I_IN_SCOPE(c, loc, BUILD_SET, 0);
@@ -1659,7 +1658,7 @@ codegen_class_body(compiler *c, stmt_ty s, int firstlineno)
16591658
/* Set __classdictcell__ if necessary */
16601659
if (SYMTABLE_ENTRY(c)->ste_needs_classdict) {
16611660
/* Store __classdictcell__ into class namespace */
1662-
int i = _PyCompile_LookupCellvar(c, &_Py_ID(__classdict__));
1661+
int i = _PyCompile_LookupCellvar(c, &_Py_ID(__classdictcell__));
16631662
RETURN_IF_ERROR_IN_SCOPE(c, i);
16641663
ADDOP_I_IN_SCOPE(c, NO_LOCATION, LOAD_CLOSURE, i);
16651664
RETURN_IF_ERROR_IN_SCOPE(
@@ -1668,7 +1667,7 @@ codegen_class_body(compiler *c, stmt_ty s, int firstlineno)
16681667
/* Return __classcell__ if it is referenced, otherwise return None */
16691668
if (SYMTABLE_ENTRY(c)->ste_needs_class_closure) {
16701669
/* Store __classcell__ into class namespace & return it */
1671-
int i = _PyCompile_LookupCellvar(c, &_Py_ID(__class__));
1670+
int i = _PyCompile_LookupCellvar(c, &_Py_ID(__classcell__));
16721671
RETURN_IF_ERROR_IN_SCOPE(c, i);
16731672
ADDOP_I_IN_SCOPE(c, NO_LOCATION, LOAD_CLOSURE, i);
16741673
ADDOP_I_IN_SCOPE(c, NO_LOCATION, COPY, 1);

‎Python/compile.c‎

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -701,20 +701,22 @@ _PyCompile_EnterScope(compiler *c, identifier name, int scope_type,
701701
return ERROR;
702702
}
703703
if (u->u_ste->ste_needs_class_closure) {
704-
/* Cook up an implicit __class__ cell. */
704+
/* Private cellvar name so it cannot clash with a class-body
705+
* LOCAL/FREE named __class__. Methods still expose freevar
706+
* __class__; _PyCompile_LookupArg maps that free to this cell. */
705707
Py_ssize_t res;
706708
assert(u->u_scope_type == COMPILE_SCOPE_CLASS);
707-
res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__class__));
709+
res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__classcell__));
708710
if (res < 0) {
709711
compiler_unit_free(u);
710712
return ERROR;
711713
}
712714
}
713715
if (u->u_ste->ste_needs_classdict) {
714-
/* Cook up an implicit __classdict__ cell. */
716+
/* Private cellvar name; see ste_needs_class_closure above. */
715717
Py_ssize_t res;
716718
assert(u->u_scope_type == COMPILE_SCOPE_CLASS);
717-
res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__classdict__));
719+
res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__classdictcell__));
718720
if (res < 0) {
719721
compiler_unit_free(u);
720722
return ERROR;
@@ -1010,11 +1012,35 @@ compiler_resolve_inlined_free(PySTEntryObject **ste, PyObject *name)
10101012
return scope;
10111013
}
10121014

1015+
/* Class-closure cells use private cellvar names that do not collide with
1016+
* class-body locals/frees of the same spelling. Method freevars keep the
1017+
* public names (__class__, ...); map those to the private cellvar. */
1018+
static PyObject *
1019+
compiler_class_closure_cellvar(PyObject *name)
1020+
{
1021+
if (name == &_Py_ID(__class__)) {
1022+
return &_Py_ID(__classcell__);
1023+
}
1024+
if (name == &_Py_ID(__classdict__)) {
1025+
return &_Py_ID(__classdictcell__);
1026+
}
1027+
if (name == &_Py_ID(__conditional_annotations__)) {
1028+
return &_Py_ID(__conditional_annotations__);
1029+
}
1030+
return NULL;
1031+
}
1032+
10131033
int
10141034
_PyCompile_GetRefType(compiler *c, PyObject *name)
10151035
{
1016-
if (c->u->u_scope_type == COMPILE_SCOPE_CLASS && _PyST_IsClassClosureName(name)) {
1017-
return CELL;
1036+
/* Synthetic cells cooked into u_cellvars under their public spelling
1037+
* (today only __conditional_annotations__) are not always in ste_symbols. */
1038+
if (c->u->u_scope_type == COMPILE_SCOPE_CLASS ||
1039+
c->u->u_scope_type == COMPILE_SCOPE_MODULE) {
1040+
PyObject *cell_name = compiler_class_closure_cellvar(name);
1041+
if (cell_name != NULL && cell_name == name) {
1042+
return CELL;
1043+
}
10181044
}
10191045
PySTEntryObject *ste = c->u->u_ste;
10201046
int scope = compiler_resolve_inlined_free(&ste, name);
@@ -1054,6 +1080,61 @@ _PyCompile_LookupCellvar(compiler *c, PyObject *name)
10541080
int
10551081
_PyCompile_LookupArg(compiler *c, PyCodeObject *co, PyObject *name)
10561082
{
1083+
/* Class units store the synthetic class-closure cell under a private
1084+
* name (__classcell__, ...). Inside an inlined comprehension, a nested
1085+
* free of the public name usually captures that comprehension's CELL;
1086+
* elsewhere (methods, class-body lambdas) it is the private cell. */
1087+
if (c->u->u_scope_type == COMPILE_SCOPE_CLASS) {
1088+
PyObject *cell_name = compiler_class_closure_cellvar(name);
1089+
if (cell_name != NULL) {
1090+
int arg;
1091+
int in_inlined = (c->u->u_ste->ste_type == InlinedComprehensionBlock);
1092+
if (in_inlined) {
1093+
arg = dict_lookup_arg(c->u->u_metadata.u_cellvars, name);
1094+
if (arg != -1) {
1095+
return arg;
1096+
}
1097+
if (PyErr_Occurred()) {
1098+
return ERROR;
1099+
}
1100+
}
1101+
if (cell_name != name) {
1102+
arg = dict_lookup_arg(c->u->u_metadata.u_cellvars, cell_name);
1103+
if (arg != -1) {
1104+
return arg;
1105+
}
1106+
if (PyErr_Occurred()) {
1107+
return ERROR;
1108+
}
1109+
}
1110+
if (!in_inlined) {
1111+
arg = dict_lookup_arg(c->u->u_metadata.u_cellvars, name);
1112+
if (arg != -1) {
1113+
return arg;
1114+
}
1115+
if (PyErr_Occurred()) {
1116+
return ERROR;
1117+
}
1118+
}
1119+
arg = dict_lookup_arg(c->u->u_metadata.u_freevars, name);
1120+
if (arg != -1 || PyErr_Occurred()) {
1121+
return arg;
1122+
}
1123+
PyObject *freevars = _PyCode_GetFreevars(co);
1124+
if (freevars == NULL) {
1125+
PyErr_Clear();
1126+
}
1127+
PyErr_Format(PyExc_SystemError,
1128+
"compiler_lookup_arg(name=%R) failed in class %S; "
1129+
"freevars of code %S: %R",
1130+
name,
1131+
c->u->u_metadata.u_name,
1132+
co->co_name,
1133+
freevars);
1134+
Py_XDECREF(freevars);
1135+
return ERROR;
1136+
}
1137+
}
10571138
/* Special case: If a class contains a method with a
10581139
* free variable that has the same name as a method,
10591140
* the name will be considered free *and* local in the

0 commit comments

Comments
 (0)