diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index ef83f662788fe4..75c109aa5b6609 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -401,6 +401,12 @@ Deprecated (as has always been the case for an executing frame). (Contributed by Irit Katriel in :gh:`79932`.) +* Assignment to a function's :attr:`__code__` attribute where the new code + object's type does not match the function's type, is deprecated. The + different types are: plain function, generator, async generator and + coroutine. + (Contributed by Irit Katriel in :gh:`81137`.) + Pending Removal in Python 3.14 ------------------------------ diff --git a/Lib/test/test_funcattrs.py b/Lib/test/test_funcattrs.py index 35b473d5e9a0b4..b3fc5ad42e7fde 100644 --- a/Lib/test/test_funcattrs.py +++ b/Lib/test/test_funcattrs.py @@ -2,6 +2,7 @@ import types import typing import unittest +import warnings def global_function(): @@ -70,6 +71,27 @@ def test(): pass test.__code__ = self.b.__code__ self.assertEqual(test(), 3) # self.b always returns 3, arbitrarily + def test_invalid___code___assignment(self): + def A(): pass + def B(): yield + async def C(): yield + async def D(x): await x + + for src in [A, B, C, D]: + for dst in [A, B, C, D]: + if src == dst: + continue + + assert src.__code__.co_flags != dst.__code__.co_flags + prev = dst.__code__ + try: + with self.assertWarnsRegex(DeprecationWarning, 'code object of non-matching type'): + dst.__code__ = src.__code__ + finally: + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', '', DeprecationWarning) + dst.__code__ = prev + def test___globals__(self): self.assertIs(self.b.__globals__, globals()) self.cannot_set_attr(self.b, '__globals__', 2, diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-11-07-12-59-02.gh-issue-81137.qFpJCY.rst b/Misc/NEWS.d/next/Core and Builtins/2023-11-07-12-59-02.gh-issue-81137.qFpJCY.rst new file mode 100644 index 00000000000000..5ca1dda63cb222 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-11-07-12-59-02.gh-issue-81137.qFpJCY.rst @@ -0,0 +1,2 @@ +Deprecate assignment to a function's ``__code__`` field when the new code +object is of a mismatched type (e.g., from a generator to a plain function). diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 8ce1bff7660618..b3701ebc9de111 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -557,6 +557,20 @@ func_set_code(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored)) nclosure, nfree); return -1; } + + PyObject *func_code = PyFunction_GET_CODE(op); + int old_flags = ((PyCodeObject *)func_code)->co_flags; + int new_flags = ((PyCodeObject *)value)->co_flags; + int mask = CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR; + if ((old_flags & mask) != (new_flags & mask)) { + if (PyErr_Warn(PyExc_DeprecationWarning, + "Assigning a code object of non-matching type is deprecated " + "(e.g., from a generator to a plain function)") < 0) + { + return -1; + } + } + handle_func_event(PyFunction_EVENT_MODIFY_CODE, op, value); _PyFunction_SetVersion(op, 0); Py_XSETREF(op->func_code, Py_NewRef(value));