Skip to content

gh-58577: make function __closure__ writable #104946

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ Callable types
| | arbitrary function | |
| | attributes. | |
+-------------------------+-------------------------------+-----------+
| :attr:`__closure__` | ``None`` or a tuple of cells | Read-only |
| :attr:`__closure__` | ``None`` or a tuple of cells | Writable |
| | that contain bindings for the | |
| | function's free variables. | |
| | See below for information on | |
Expand Down
28 changes: 25 additions & 3 deletions Lib/test/test_funcattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,35 @@ def func3(s): pass

def test___closure__(self):
a = 12
def f(): print(a)
b = 67
def f():
return a + b
c = f.__closure__
self.assertIsInstance(c, tuple)
self.assertEqual(len(c), 1)
self.assertEqual(len(c), 2)
# don't have a type object handy
self.assertEqual(c[0].__class__.__name__, "cell")
self.cannot_set_attr(f, "__closure__", c, AttributeError)

# check that we can overwrite the __closure__
self.assertEqual(f(), 79)
good_closure = f.__closure__ = (cell(42), cell(34))
self.assertEqual(f(), 76)

# check that we can't write anything besides a 2-tuple of cells or None
with self.assertRaises(TypeError):
f.__closure__ = 'spam' # wrong type
with self.assertRaises(ValueError):
f.__closure__ = () # too short
with self.assertRaises(ValueError):
f.__closure__ = (cell(1),) # too short
with self.assertRaises(ValueError):
f.__closure__ = (cell(1), cell(2), cell(3)) # too long
with self.assertRaises(TypeError):
f.__closure__ = (cell(1), 2) # wrong element type

# __closure__ should not be affected by the previous exceptions
self.assertIs(f.__closure__, good_closure)
self.assertEqual(f(), 76)

def test_cell_new(self):
cell_obj = types.CellType(1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make function :attr:`__closure__` writable. Patched by Richard Oudkerk and Furkan Onder.
57 changes: 56 additions & 1 deletion Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,6 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations)
#define OFF(x) offsetof(PyFunctionObject, x)

static PyMemberDef func_memberlist[] = {
{"__closure__", _Py_T_OBJECT, OFF(func_closure), Py_READONLY},
{"__doc__", _Py_T_OBJECT, OFF(func_doc), 0},
{"__globals__", _Py_T_OBJECT, OFF(func_globals), Py_READONLY},
{"__module__", _Py_T_OBJECT, OFF(func_module), 0},
Expand Down Expand Up @@ -692,8 +691,64 @@ _Py_set_function_type_params(PyThreadState *Py_UNUSED(ignored), PyObject *func,
return Py_NewRef(func);
}

static PyObject *
func_get_closure(PyFunctionObject *op, void *Py_UNUSED(ignored))
{
if (op->func_closure == NULL) {
Py_INCREF(Py_None);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Py_None is immortal now, but I don't know what the policy is about its use in Objects/.

I think that people still use it.

return Py_None;
}
Py_INCREF(op->func_closure);
return op->func_closure;
}

static int
func_set_closure(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
{
PyObject *tmp;
Py_ssize_t nfree, nclosure;
PyCodeObject *code;

if (value != Py_None && !PyTuple_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"__closure__ must be set to a tuple object or None");
return -1;
}

code = (PyCodeObject*)op->func_code;
nfree = PyTuple_GET_SIZE(PyCode_GetFreevars(code));
nclosure = value == Py_None ? 0 : PyTuple_GET_SIZE(value);

if (nfree != nclosure) {
PyErr_Format(PyExc_ValueError,
"%U requires closure of length %zd, not %zd",
code->co_name, nfree, nclosure);
return -1;
}

if (nclosure) {
Py_ssize_t i;
for (i = 0; i < nclosure; i++) {
PyObject *o = PyTuple_GET_ITEM(value, i);
if (!PyCell_Check(o)) {
PyErr_Format(PyExc_TypeError,
"expected only cells in closure, found %s",
o->ob_type->tp_name);
return -1;
}
}
}

tmp = op->func_closure;
Py_XINCREF(value);
op->func_closure = value;
Py_XDECREF(tmp);
return 0;
}

static PyGetSetDef func_getsetlist[] = {
{"__code__", (getter)func_get_code, (setter)func_set_code},
{"__closure__", (getter)func_get_closure, (setter)func_set_closure},
{"__defaults__", (getter)func_get_defaults,
(setter)func_set_defaults},
{"__kwdefaults__", (getter)func_get_kwdefaults,
Expand Down