Skip to content

Commit

Permalink
gh-86682: Adds sys._getframemodulename as an alternative to using _ge…
Browse files Browse the repository at this point in the history
…tframe (GH-99520)

Also updates calls in collections, doctest, enum, and typing modules to use _getframemodulename first when available.
  • Loading branch information
zooba authored Jan 13, 2023
1 parent 94fc770 commit b5d4347
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 14 deletions.
16 changes: 16 additions & 0 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,22 @@ always available.
It is not guaranteed to exist in all implementations of Python.


.. function:: _getframemodulename([depth])

Return the name of a module from the call stack. If optional integer *depth*
is given, return the module that many calls below the top of the stack. If
that is deeper than the call stack, or if the module is unidentifiable,
``None`` is returned. The default for *depth* is zero, returning the
module at the top of the call stack.

.. audit-event:: sys._getframemodulename depth sys._getframemodulename

.. 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
2 changes: 1 addition & 1 deletion Lib/_pydecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@

try:
from collections import namedtuple as _namedtuple
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent')
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent', module='decimal')
except ImportError:
DecimalTuple = lambda *args: args

Expand Down
9 changes: 6 additions & 3 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,12 @@ def __getnewargs__(self):
# specified a particular module.
if module is None:
try:
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
module = _sys._getframemodulename(1) or '__main__'
except AttributeError:
try:
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
if module is not None:
result.__module__ = module

Expand Down
8 changes: 7 additions & 1 deletion Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,13 @@ 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__']]
try:
try:
return sys.modules[sys._getframemodulename(depth)]
except AttributeError:
return sys.modules[sys._getframe(depth).f_globals['__name__']]
except KeyError:
pass
else:
raise TypeError("Expected a module, string, or None")

Expand Down
12 changes: 7 additions & 5 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,13 +862,15 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s
member_name, member_value = item
classdict[member_name] = member_value

# TODO: replace the frame hack if a blessed way to know the calling
# module is ever developed
if module is None:
try:
module = sys._getframe(2).f_globals['__name__']
except (AttributeError, ValueError, KeyError):
pass
module = sys._getframemodulename(2)
except AttributeError:
# Fall back on _getframe if _getframemodulename is missing
try:
module = sys._getframe(2).f_globals['__name__']
except (AttributeError, ValueError, KeyError):
pass
if module is None:
_make_class_unpicklable(classdict)
else:
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/audit-tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,17 @@ def hook(event, args):
sys._getframe()


def test_sys_getframemodulename():
import sys

def hook(event, args):
if event.startswith("sys."):
print(event, *args)

sys.addaudithook(hook)
sys._getframemodulename()


def test_threading():
import _thread

Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ def test_sys_getframe(self):

self.assertEqual(actual, expected)

def test_sys_getframemodulename(self):
returncode, events, stderr = self.run_python("test_sys_getframemodulename")
if returncode:
self.fail(stderr)

if support.verbose:
print(*events, sep='\n')
actual = [(ev[0], ev[2]) for ev in events]
expected = [("sys._getframemodulename", "0")]

self.assertEqual(actual, expected)


def test_threading(self):
returncode, events, stderr = self.run_python("test_threading")
Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,26 @@ def test_getframe(self):
is sys._getframe().f_code
)

def test_getframemodulename(self):
# Default depth gets ourselves
self.assertEqual(__name__, sys._getframemodulename())
self.assertEqual("unittest.case", sys._getframemodulename(1))
i = 0
f = sys._getframe(i)
while f:
self.assertEqual(
f.f_globals['__name__'],
sys._getframemodulename(i) or '__main__'
)
i += 1
f2 = f.f_back
try:
f = sys._getframe(i)
except ValueError:
break
self.assertIs(f, f2)
self.assertIsNone(sys._getframemodulename(i))

# sys._current_frames() is a CPython-only gimmick.
@threading_helper.reap_threads
@threading_helper.requires_working_threading()
Expand Down
8 changes: 6 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1939,11 +1939,15 @@ def _no_init_or_replace_init(self, *args, **kwargs):


def _caller(depth=1, default='__main__'):
try:
return sys._getframemodulename(depth + 1) or default
except AttributeError: # For platforms without _getframemodulename()
pass
try:
return sys._getframe(depth + 1).f_globals.get('__name__', default)
except (AttributeError, ValueError): # For platforms without _getframe()
return None

pass
return None

def _allow_reckless_class_checks(depth=3):
"""Allow instance and class checks for special stdlib modules.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ensure runtime-created collections have the correct module name using
the newly added (internal) :func:`sys._getframemodulename`.
5 changes: 4 additions & 1 deletion Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
op->func_doc = Py_NewRef(Py_None);
op->func_dict = NULL;
op->func_weakreflist = NULL;
op->func_module = NULL;
op->func_module = Py_XNewRef(PyDict_GetItem(op->func_globals, &_Py_ID(__name__)));
if (!op->func_module) {
PyErr_Clear();
}
op->func_annotations = NULL;
op->vectorcall = _PyFunction_Vectorcall;
op->func_version = 0;
Expand Down
71 changes: 70 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.

38 changes: 38 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,43 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
}


/*[clinic input]
sys._getframemodulename
depth: int = 0
Return the name of the module for a calling frame.
The default depth returns the module containing the call to this API.
A more typical use in a library will pass a depth of 1 to get the user's
module rather than the library module.
If no frame, module, or name can be found, returns None.
[clinic start generated code]*/

static PyObject *
sys__getframemodulename_impl(PyObject *module, int depth)
/*[clinic end generated code: output=1d70ef691f09d2db input=d4f1a8ed43b8fb46]*/
{
if (PySys_Audit("sys._getframemodulename", "i", depth) < 0) {
return NULL;
}
_PyInterpreterFrame *f = _PyThreadState_GET()->cframe->current_frame;
while (f && (_PyFrame_IsIncomplete(f) || depth-- > 0)) {
f = f->previous;
}
if (f == NULL || f->f_funcobj == NULL) {
Py_RETURN_NONE;
}
PyObject *r = PyFunction_GetModule(f->f_funcobj);
if (!r) {
PyErr_Clear();
r = Py_None;
}
return Py_NewRef(r);
}


static PyMethodDef sys_methods[] = {
/* Might as well keep this in alphabetic order */
SYS_ADDAUDITHOOK_METHODDEF
Expand Down Expand Up @@ -2200,6 +2237,7 @@ static PyMethodDef sys_methods[] = {
{"getsizeof", _PyCFunction_CAST(sys_getsizeof),
METH_VARARGS | METH_KEYWORDS, getsizeof_doc},
SYS__GETFRAME_METHODDEF
SYS__GETFRAMEMODULENAME_METHODDEF
SYS_GETWINDOWSVERSION_METHODDEF
SYS__ENABLELEGACYWINDOWSFSENCODING_METHODDEF
SYS_INTERN_METHODDEF
Expand Down

0 comments on commit b5d4347

Please sign in to comment.