summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/test_code.py45
-rw-r--r--Misc/NEWS.d/next/Core and Builtins/2017-12-02-21-37-22.bpo-32176.Wt25-N.rst5
-rw-r--r--Objects/codeobject.c10
-rw-r--r--Python/compile.c5
4 files changed, 59 insertions, 6 deletions
diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py
index 90cb584..55faf4c 100644
--- a/Lib/test/test_code.py
+++ b/Lib/test/test_code.py
@@ -102,6 +102,7 @@ consts: ('None',)
"""
+import inspect
import sys
import threading
import unittest
@@ -130,6 +131,10 @@ def dump(co):
print("%s: %s" % (attr, getattr(co, "co_" + attr)))
print("consts:", tuple(consts(co.co_consts)))
+# Needed for test_closure_injection below
+# Defined at global scope to avoid implicitly closing over __class__
+def external_getitem(self, i):
+ return f"Foreign getitem: {super().__getitem__(i)}"
class CodeTest(unittest.TestCase):
@@ -141,6 +146,46 @@ class CodeTest(unittest.TestCase):
self.assertEqual(co.co_name, "funcname")
self.assertEqual(co.co_firstlineno, 15)
+ @cpython_only
+ def test_closure_injection(self):
+ # From https://bugs.python.org/issue32176
+ from types import FunctionType, CodeType
+
+ def create_closure(__class__):
+ return (lambda: __class__).__closure__
+
+ def new_code(c):
+ '''A new code object with a __class__ cell added to freevars'''
+ return CodeType(
+ c.co_argcount, c.co_kwonlyargcount, c.co_nlocals,
+ c.co_stacksize, c.co_flags, c.co_code, c.co_consts, c.co_names,
+ c.co_varnames, c.co_filename, c.co_name, c.co_firstlineno,
+ c.co_lnotab, c.co_freevars + ('__class__',), c.co_cellvars)
+
+ def add_foreign_method(cls, name, f):
+ code = new_code(f.__code__)
+ assert not f.__closure__
+ closure = create_closure(cls)
+ defaults = f.__defaults__
+ setattr(cls, name, FunctionType(code, globals(), name, defaults, closure))
+
+ class List(list):
+ pass
+
+ add_foreign_method(List, "__getitem__", external_getitem)
+
+ # Ensure the closure injection actually worked
+ function = List.__getitem__
+ class_ref = function.__closure__[0].cell_contents
+ self.assertIs(class_ref, List)
+
+ # Ensure the code correctly indicates it accesses a free variable
+ self.assertFalse(function.__code__.co_flags & inspect.CO_NOFREE,
+ hex(function.__code__.co_flags))
+
+ # Ensure the zero-arg super() call in the injected method works
+ obj = List([1, 2, 3])
+ self.assertEqual(obj[0], "Foreign getitem: 1")
def isinterned(s):
return s is sys.intern(('_' + s + '_')[1:-1])
diff --git a/Misc/NEWS.d/next/Core and Builtins/2017-12-02-21-37-22.bpo-32176.Wt25-N.rst b/Misc/NEWS.d/next/Core and Builtins/2017-12-02-21-37-22.bpo-32176.Wt25-N.rst
new file mode 100644
index 0000000..9d56711
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2017-12-02-21-37-22.bpo-32176.Wt25-N.rst
@@ -0,0 +1,5 @@
+co_flags.CO_NOFREE is now always set correctly by the code object
+constructor based on freevars and cellvars, rather than needing to be set
+correctly by the caller. This ensures it will be cleared automatically when
+additional cell references are injected into a modified code object and
+function.
diff --git a/Objects/codeobject.c b/Objects/codeobject.c
index f312f33..0509b8e 100644
--- a/Objects/codeobject.c
+++ b/Objects/codeobject.c
@@ -124,12 +124,20 @@ PyCode_New(int argcount, int kwonlyargcount,
if (PyUnicode_READY(filename) < 0)
return NULL;
- n_cellvars = PyTuple_GET_SIZE(cellvars);
intern_strings(names);
intern_strings(varnames);
intern_strings(freevars);
intern_strings(cellvars);
intern_string_constants(consts);
+
+ /* Check for any inner or outer closure references */
+ n_cellvars = PyTuple_GET_SIZE(cellvars);
+ if (!n_cellvars && !PyTuple_GET_SIZE(freevars)) {
+ flags |= CO_NOFREE;
+ } else {
+ flags &= ~CO_NOFREE;
+ }
+
/* Create mapping between cells and arguments if needed. */
if (n_cellvars) {
Py_ssize_t total_args = argcount + kwonlyargcount +
diff --git a/Python/compile.c b/Python/compile.c
index a3ea60d..a3fcd53 100644
--- a/Python/compile.c
+++ b/Python/compile.c
@@ -5273,11 +5273,6 @@ compute_code_flags(struct compiler *c)
/* (Only) inherit compilerflags in PyCF_MASK */
flags |= (c->c_flags->cf_flags & PyCF_MASK);
- if (!PyDict_GET_SIZE(c->u->u_freevars) &&
- !PyDict_GET_SIZE(c->u->u_cellvars)) {
- flags |= CO_NOFREE;
- }
-
return flags;
}