diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 5e47201f88eae1..1c08e5b18ed103 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -750,6 +750,10 @@ always available. that is deeper than the call stack, :exc:`ValueError` is raised. The default for *depth* is zero, returning the frame at the top of the call stack. + Unlike the similar :func:`_getcaller` (new in Python 3.11), this returns a + full frame object, making it more expensive to execute and harder to port + to alternative implementations of Python. + .. audit-event:: sys._getframe "" sys._getframe .. impl-detail:: @@ -758,6 +762,23 @@ always available. It is not guaranteed to exist in all implementations of Python. +.. function:: _getcaller([depth]) + + Return a function object from the call stack. If optional integer *depth* is + positive, return the function object that many calls below the top of the stack. If + that is deeper than the call stack, :exc:`ValueError` is raised. The default + for *depth* is zero, returning the currently executing function. + + .. versionadded:: 3.11 + + .. audit-event:: sys._getcaller "" sys._getcaller + + .. impl-detail:: + + This function should be used for internal and specialized purposes only. + It is not guaranteed to exist in all implementations of Python. + + .. function:: getprofile() .. index:: diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 264435cb41b2f4..f5383ec681d2f8 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -490,12 +490,12 @@ def __getnewargs__(self): # For pickling to work, the __module__ variable needs to be set to the frame # where the named tuple is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython), or where the user has + # sys._getcaller is not defined or sys._getcaller is not + # defined for arguments greater than 0, or where the user has # specified a particular module. if module is None: try: - module = _sys._getframe(1).f_globals.get('__name__', '__main__') + module = _sys._getcaller(1).__globals__.get('__name__', '__main__') except (AttributeError, ValueError): pass if module is not None: diff --git a/Lib/doctest.py b/Lib/doctest.py index 4735b59852685c..5df59333887d15 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -207,7 +207,7 @@ def _normalize_module(module, depth=2): elif isinstance(module, str): return __import__(module, globals(), locals(), ["*"]) elif module is None: - return sys.modules[sys._getframe(depth).f_globals['__name__']] + return sys.modules[sys._getcaller(depth).__globals__['__name__']] else: raise TypeError("Expected a module, string, or None") diff --git a/Lib/enum.py b/Lib/enum.py index 85245c95f9a9c7..94eed268440f12 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -904,7 +904,7 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s # module is ever developed if module is None: try: - module = sys._getframe(2).f_globals['__name__'] + module = sys._getcaller(2).__globals__['__name__'] except (AttributeError, ValueError, KeyError): pass if module is None: diff --git a/Lib/test/sys_getcaller.py b/Lib/test/sys_getcaller.py new file mode 100644 index 00000000000000..b8962180c2e59e --- /dev/null +++ b/Lib/test/sys_getcaller.py @@ -0,0 +1,3 @@ +import sys + +func = sys._getcaller(0) diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 21d7ccbf709666..54f4d1291049ab 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -399,6 +399,27 @@ def test_getframe(self): is sys._getframe().f_code ) + def test_getcaller(self): + self.assertRaises(TypeError, sys._getcaller, 42, 42) + self.assertRaises(ValueError, sys._getcaller, 2000000000) + self.assertIs(SysModuleTest.test_getcaller, sys._getcaller(0)) + + def save_parent(): + return sys._getcaller(1) + + self.assertIs(SysModuleTest.test_getcaller, save_parent()) + + class X: + func = sys._getcaller(0) + func2 = save_parent() + + self.assertEqual(X.func.__name__, "X") + self.assertIs(X.func, X.func2) + + from test import sys_getcaller + + self.assertEqual(sys_getcaller.func.__name__, "") + # sys._current_frames() is a CPython-only gimmick. @threading_helper.reap_threads def test_current_frames(self): diff --git a/Lib/typing.py b/Lib/typing.py index 0ee5c8596a0214..98fdd85d33bfb8 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1557,8 +1557,8 @@ def _no_init_or_replace_init(self, *args, **kwargs): def _caller(depth=1, default='__main__'): try: - return sys._getframe(depth + 1).f_globals.get('__name__', default) - except (AttributeError, ValueError): # For platforms without _getframe() + return sys._getcaller(depth + 1).__globals__.get('__name__', default) + except (AttributeError, ValueError): # For platforms without _getcaller() return None diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-01-26-20-00-45.bpo-46543.oIulvu.rst b/Misc/NEWS.d/next/Core and Builtins/2022-01-26-20-00-45.bpo-46543.oIulvu.rst new file mode 100644 index 00000000000000..136f2a7ccba342 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-01-26-20-00-45.bpo-46543.oIulvu.rst @@ -0,0 +1 @@ +Add :func:`sys._getcaller`. Patch by Jelle Zijlstra. diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index ce5390c8a1e588..7f8e2d570b3373 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -787,6 +787,49 @@ sys_getallocatedblocks(PyObject *module, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(sys__getcaller__doc__, +"_getcaller($module, depth=0, /)\n" +"--\n" +"\n" +"Return a function object from the call stack.\n" +"\n" +"If optional integer depth is 1 or more, return the function object that many\n" +"calls below the top of the stack. If that is deeper than the call\n" +"stack, ValueError is raised. If depth is 0, return the current function\n" +"object.\n" +"\n" +"This function should be used for internal and specialized purposes\n" +"only."); + +#define SYS__GETCALLER_METHODDEF \ + {"_getcaller", (PyCFunction)(void(*)(void))sys__getcaller, METH_FASTCALL, sys__getcaller__doc__}, + +static PyObject * +sys__getcaller_impl(PyObject *module, int depth); + +static PyObject * +sys__getcaller(PyObject *module, PyObject *const *args, Py_ssize_t nargs) +{ + PyObject *return_value = NULL; + int depth = 0; + + if (!_PyArg_CheckPositional("_getcaller", nargs, 0, 1)) { + goto exit; + } + if (nargs < 1) { + goto skip_optional; + } + depth = _PyLong_AsInt(args[0]); + if (depth == -1 && PyErr_Occurred()) { + goto exit; + } +skip_optional: + return_value = sys__getcaller_impl(module, depth); + +exit: + return return_value; +} + PyDoc_STRVAR(sys__getframe__doc__, "_getframe($module, depth=0, /)\n" "--\n" @@ -798,6 +841,10 @@ PyDoc_STRVAR(sys__getframe__doc__, "stack, ValueError is raised. The default for depth is zero, returning\n" "the frame at the top of the call stack.\n" "\n" +"Unlike the similar sys._getcaller, this returns a full frame object,\n" +"making it more expensive to execute and harder to port to alternative\n" +"implementations of Python.\n" +"\n" "This function should be used for internal and specialized purposes\n" "only."); @@ -1014,4 +1061,4 @@ sys_getandroidapilevel(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=60756bc6f683e0c8 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=32a5bed6e0473b5d input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index acb03781a4f14b..b656369d4710cc 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1799,6 +1799,46 @@ sys_getallocatedblocks_impl(PyObject *module) } +/*[clinic input] +sys._getcaller + + depth: int = 0 + / + +Return a function object from the call stack. + +If optional integer depth is 1 or more, return the function object that many +calls below the top of the stack. If that is deeper than the call +stack, ValueError is raised. If depth is 0, return the current function +object. + +This function should be used for internal and specialized purposes +only. +[clinic start generated code]*/ + +static PyObject * +sys__getcaller_impl(PyObject *module, int depth) +/*[clinic end generated code: output=250f47adb2372e4a input=242cb11f8e6d7a60]*/ +{ + PyThreadState *tstate = _PyThreadState_GET(); + InterpreterFrame *frame = tstate->cframe->current_frame; + + if (_PySys_Audit(tstate, "sys._getcaller", NULL) < 0) { + return NULL; + } + + while (depth > 0 && frame != NULL) { + frame = frame->previous; + --depth; + } + if (frame == NULL) { + _PyErr_SetString(tstate, PyExc_ValueError, + "call stack is not deep enough"); + return NULL; + } + return _Py_XNewRef((PyObject *)frame->f_func); +} + /*[clinic input] sys._getframe @@ -1812,13 +1852,17 @@ calls below the top of the stack. If that is deeper than the call stack, ValueError is raised. The default for depth is zero, returning the frame at the top of the call stack. +Unlike the similar sys._getcaller, this returns a full frame object, +making it more expensive to execute and harder to port to alternative +implementations of Python. + This function should be used for internal and specialized purposes only. [clinic start generated code]*/ static PyObject * sys__getframe_impl(PyObject *module, int depth) -/*[clinic end generated code: output=d438776c04d59804 input=c1be8a6464b11ee5]*/ +/*[clinic end generated code: output=d438776c04d59804 input=a57ecc9db9b721ad]*/ { PyThreadState *tstate = _PyThreadState_GET(); InterpreterFrame *frame = tstate->cframe->current_frame; @@ -2007,6 +2051,7 @@ static PyMethodDef sys_methods[] = { {"getsizeof", (PyCFunction)(void(*)(void))sys_getsizeof, METH_VARARGS | METH_KEYWORDS, getsizeof_doc}, SYS__GETFRAME_METHODDEF + SYS__GETCALLER_METHODDEF SYS_GETWINDOWSVERSION_METHODDEF SYS__ENABLELEGACYWINDOWSFSENCODING_METHODDEF SYS_INTERN_METHODDEF