Skip to content

bpo-46543: add sys._getcaller #30950

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

Closed
wants to merge 9 commits into from
Closed
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
21 changes: 21 additions & 0 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand All @@ -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::
Expand Down
6 changes: 3 additions & 3 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
2 changes: 1 addition & 1 deletion Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/sys_getcaller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import sys

func = sys._getcaller(0)
21 changes: 21 additions & 0 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__, "<module>")

# sys._current_frames() is a CPython-only gimmick.
@threading_helper.reap_threads
def test_current_frames(self):
Expand Down
4 changes: 2 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :func:`sys._getcaller`. Patch by Jelle Zijlstra.
49 changes: 48 additions & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 46 additions & 1 deletion Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down