diff options
-rw-r--r-- | Lib/test/test_code.py | 45 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Core and Builtins/2017-12-02-21-37-22.bpo-32176.Wt25-N.rst | 5 | ||||
-rw-r--r-- | Objects/codeobject.c | 10 | ||||
-rw-r--r-- | Python/compile.c | 5 |
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; } |