Skip to content

Commit b5d4347

Browse files
authored
gh-86682: Adds sys._getframemodulename as an alternative to using _getframe (GH-99520)
Also updates calls in collections, doctest, enum, and typing modules to use _getframemodulename first when available.
1 parent 94fc770 commit b5d4347

File tree

13 files changed

+200
-14
lines changed

13 files changed

+200
-14
lines changed

Doc/library/sys.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,22 @@ always available.
808808
It is not guaranteed to exist in all implementations of Python.
809809

810810

811+
.. function:: _getframemodulename([depth])
812+
813+
Return the name of a module from the call stack. If optional integer *depth*
814+
is given, return the module that many calls below the top of the stack. If
815+
that is deeper than the call stack, or if the module is unidentifiable,
816+
``None`` is returned. The default for *depth* is zero, returning the
817+
module at the top of the call stack.
818+
819+
.. audit-event:: sys._getframemodulename depth sys._getframemodulename
820+
821+
.. impl-detail::
822+
823+
This function should be used for internal and specialized purposes only.
824+
It is not guaranteed to exist in all implementations of Python.
825+
826+
811827
.. function:: getprofile()
812828

813829
.. index::

Lib/_pydecimal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159

160160
try:
161161
from collections import namedtuple as _namedtuple
162-
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent')
162+
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent', module='decimal')
163163
except ImportError:
164164
DecimalTuple = lambda *args: args
165165

Lib/collections/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,9 +507,12 @@ def __getnewargs__(self):
507507
# specified a particular module.
508508
if module is None:
509509
try:
510-
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
511-
except (AttributeError, ValueError):
512-
pass
510+
module = _sys._getframemodulename(1) or '__main__'
511+
except AttributeError:
512+
try:
513+
module = _sys._getframe(1).f_globals.get('__name__', '__main__')
514+
except (AttributeError, ValueError):
515+
pass
513516
if module is not None:
514517
result.__module__ = module
515518

Lib/doctest.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,13 @@ def _normalize_module(module, depth=2):
207207
elif isinstance(module, str):
208208
return __import__(module, globals(), locals(), ["*"])
209209
elif module is None:
210-
return sys.modules[sys._getframe(depth).f_globals['__name__']]
210+
try:
211+
try:
212+
return sys.modules[sys._getframemodulename(depth)]
213+
except AttributeError:
214+
return sys.modules[sys._getframe(depth).f_globals['__name__']]
215+
except KeyError:
216+
pass
211217
else:
212218
raise TypeError("Expected a module, string, or None")
213219

Lib/enum.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -862,13 +862,15 @@ def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, s
862862
member_name, member_value = item
863863
classdict[member_name] = member_value
864864

865-
# TODO: replace the frame hack if a blessed way to know the calling
866-
# module is ever developed
867865
if module is None:
868866
try:
869-
module = sys._getframe(2).f_globals['__name__']
870-
except (AttributeError, ValueError, KeyError):
871-
pass
867+
module = sys._getframemodulename(2)
868+
except AttributeError:
869+
# Fall back on _getframe if _getframemodulename is missing
870+
try:
871+
module = sys._getframe(2).f_globals['__name__']
872+
except (AttributeError, ValueError, KeyError):
873+
pass
872874
if module is None:
873875
_make_class_unpicklable(classdict)
874876
else:

Lib/test/audit-tests.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,17 @@ def hook(event, args):
419419
sys._getframe()
420420

421421

422+
def test_sys_getframemodulename():
423+
import sys
424+
425+
def hook(event, args):
426+
if event.startswith("sys."):
427+
print(event, *args)
428+
429+
sys.addaudithook(hook)
430+
sys._getframemodulename()
431+
432+
422433
def test_threading():
423434
import _thread
424435

Lib/test/test_audit.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,18 @@ def test_sys_getframe(self):
186186

187187
self.assertEqual(actual, expected)
188188

189+
def test_sys_getframemodulename(self):
190+
returncode, events, stderr = self.run_python("test_sys_getframemodulename")
191+
if returncode:
192+
self.fail(stderr)
193+
194+
if support.verbose:
195+
print(*events, sep='\n')
196+
actual = [(ev[0], ev[2]) for ev in events]
197+
expected = [("sys._getframemodulename", "0")]
198+
199+
self.assertEqual(actual, expected)
200+
189201

190202
def test_threading(self):
191203
returncode, events, stderr = self.run_python("test_threading")

Lib/test/test_sys.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,26 @@ def test_getframe(self):
399399
is sys._getframe().f_code
400400
)
401401

402+
def test_getframemodulename(self):
403+
# Default depth gets ourselves
404+
self.assertEqual(__name__, sys._getframemodulename())
405+
self.assertEqual("unittest.case", sys._getframemodulename(1))
406+
i = 0
407+
f = sys._getframe(i)
408+
while f:
409+
self.assertEqual(
410+
f.f_globals['__name__'],
411+
sys._getframemodulename(i) or '__main__'
412+
)
413+
i += 1
414+
f2 = f.f_back
415+
try:
416+
f = sys._getframe(i)
417+
except ValueError:
418+
break
419+
self.assertIs(f, f2)
420+
self.assertIsNone(sys._getframemodulename(i))
421+
402422
# sys._current_frames() is a CPython-only gimmick.
403423
@threading_helper.reap_threads
404424
@threading_helper.requires_working_threading()

Lib/typing.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,11 +1939,15 @@ def _no_init_or_replace_init(self, *args, **kwargs):
19391939

19401940

19411941
def _caller(depth=1, default='__main__'):
1942+
try:
1943+
return sys._getframemodulename(depth + 1) or default
1944+
except AttributeError: # For platforms without _getframemodulename()
1945+
pass
19421946
try:
19431947
return sys._getframe(depth + 1).f_globals.get('__name__', default)
19441948
except (AttributeError, ValueError): # For platforms without _getframe()
1945-
return None
1946-
1949+
pass
1950+
return None
19471951

19481952
def _allow_reckless_class_checks(depth=3):
19491953
"""Allow instance and class checks for special stdlib modules.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Ensure runtime-created collections have the correct module name using
2+
the newly added (internal) :func:`sys._getframemodulename`.

Objects/funcobject.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
9393
op->func_doc = Py_NewRef(Py_None);
9494
op->func_dict = NULL;
9595
op->func_weakreflist = NULL;
96-
op->func_module = NULL;
96+
op->func_module = Py_XNewRef(PyDict_GetItem(op->func_globals, &_Py_ID(__name__)));
97+
if (!op->func_module) {
98+
PyErr_Clear();
99+
}
97100
op->func_annotations = NULL;
98101
op->vectorcall = _PyFunction_Vectorcall;
99102
op->func_version = 0;

Python/clinic/sysmodule.c.h

Lines changed: 70 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/sysmodule.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2172,6 +2172,43 @@ sys_is_stack_trampoline_active_impl(PyObject *module)
21722172
}
21732173

21742174

2175+
/*[clinic input]
2176+
sys._getframemodulename
2177+
2178+
depth: int = 0
2179+
2180+
Return the name of the module for a calling frame.
2181+
2182+
The default depth returns the module containing the call to this API.
2183+
A more typical use in a library will pass a depth of 1 to get the user's
2184+
module rather than the library module.
2185+
2186+
If no frame, module, or name can be found, returns None.
2187+
[clinic start generated code]*/
2188+
2189+
static PyObject *
2190+
sys__getframemodulename_impl(PyObject *module, int depth)
2191+
/*[clinic end generated code: output=1d70ef691f09d2db input=d4f1a8ed43b8fb46]*/
2192+
{
2193+
if (PySys_Audit("sys._getframemodulename", "i", depth) < 0) {
2194+
return NULL;
2195+
}
2196+
_PyInterpreterFrame *f = _PyThreadState_GET()->cframe->current_frame;
2197+
while (f && (_PyFrame_IsIncomplete(f) || depth-- > 0)) {
2198+
f = f->previous;
2199+
}
2200+
if (f == NULL || f->f_funcobj == NULL) {
2201+
Py_RETURN_NONE;
2202+
}
2203+
PyObject *r = PyFunction_GetModule(f->f_funcobj);
2204+
if (!r) {
2205+
PyErr_Clear();
2206+
r = Py_None;
2207+
}
2208+
return Py_NewRef(r);
2209+
}
2210+
2211+
21752212
static PyMethodDef sys_methods[] = {
21762213
/* Might as well keep this in alphabetic order */
21772214
SYS_ADDAUDITHOOK_METHODDEF
@@ -2200,6 +2237,7 @@ static PyMethodDef sys_methods[] = {
22002237
{"getsizeof", _PyCFunction_CAST(sys_getsizeof),
22012238
METH_VARARGS | METH_KEYWORDS, getsizeof_doc},
22022239
SYS__GETFRAME_METHODDEF
2240+
SYS__GETFRAMEMODULENAME_METHODDEF
22032241
SYS_GETWINDOWSVERSION_METHODDEF
22042242
SYS__ENABLELEGACYWINDOWSFSENCODING_METHODDEF
22052243
SYS_INTERN_METHODDEF

0 commit comments

Comments
 (0)