]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-86682: Adds sys._getframemodulename as an alternative to using _getframe (GH-99520)
authorSteve Dower <steve.dower@python.org>
Fri, 13 Jan 2023 11:31:06 +0000 (11:31 +0000)
committerGitHub <noreply@github.com>
Fri, 13 Jan 2023 11:31:06 +0000 (11:31 +0000)
Also updates calls in collections, doctest, enum, and typing modules to use _getframemodulename first when available.

13 files changed:
Doc/library/sys.rst
Lib/_pydecimal.py
Lib/collections/__init__.py
Lib/doctest.py
Lib/enum.py
Lib/test/audit-tests.py
Lib/test/test_audit.py
Lib/test/test_sys.py
Lib/typing.py
Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst [new file with mode: 0644]
Objects/funcobject.c
Python/clinic/sysmodule.c.h
Python/sysmodule.c

index 28adca1f618d753417edb553f7014595cbd4c454..605e2c9a6710c1ffdc809b329aa4edae046a5c2b 100644 (file)
@@ -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::
index f9d6c9901f1f31034bdfabcdb239abdad4c384fc..2692f2fcba45bf218c05a54b48a48e8b69977d78 100644 (file)
@@ -159,7 +159,7 @@ import sys
 
 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
 
index f07ee143a5aff114cfe4b7d923bb9689a886d2ab..b5e4d16e9dbcad63039f7f877341533a9cc04fac 100644 (file)
@@ -507,9 +507,12 @@ def namedtuple(typename, field_names, *, rename=False, defaults=None, module=Non
     # 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
 
index dafad505ef0e86a11b289da0ae7a9b01ae966e84..2776d74bf9b586d8c4b6d11f19d1d768eb96414b 100644 (file)
@@ -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")
 
index 21f63881d78d4d8bc0d0b06f5166f733e93d94e3..4658393d756e07b171c21f1b1e064065a99486ef 100644 (file)
@@ -862,13 +862,15 @@ class EnumType(type):
                 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:
index bf56cea541d1216d5ac27c0bb3e852f34b30cc44..0edc9d9c472766c8357b4feca3af539083b79318 100644 (file)
@@ -419,6 +419,17 @@ def test_sys_getframe():
     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
 
index 70f8a77a4761a7b991edcf80b6189f6ded75cbb7..0b69864751d83d8281c27514cc94ea1ca9b3e543 100644 (file)
@@ -186,6 +186,18 @@ class AuditTest(unittest.TestCase):
 
         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")
index 232b79971dc2b79ea8cf55e3f50dd73e1d844849..ab1a0659471857cdaacb6f920770244bd2b34ece 100644 (file)
@@ -399,6 +399,26 @@ class SysModuleTest(unittest.TestCase):
             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()
index 8bc38f98c867543beb8fcc45efcd284714d7440c..4675af12d087b6cbb01ae117fc7bdda8eb1b4b45 100644 (file)
@@ -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.
diff --git a/Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst b/Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst
new file mode 100644 (file)
index 0000000..64ef42a
--- /dev/null
@@ -0,0 +1,2 @@
+Ensure runtime-created collections have the correct module name using
+the newly added (internal) :func:`sys._getframemodulename`.
index d5cf5b9277b3f1a3569303b6fe4f3080343f0794..baa360381a77242b3c7b1450cbc779e73f7b1fb0 100644 (file)
@@ -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;
index 03eeda8126ebbb0aac738c979485b42de0cae660..46252dd404325bb84749449ce3a079449fa35681 100644 (file)
@@ -1275,6 +1275,75 @@ sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored))
     return sys_is_stack_trampoline_active_impl(module);
 }
 
+PyDoc_STRVAR(sys__getframemodulename__doc__,
+"_getframemodulename($module, /, depth=0)\n"
+"--\n"
+"\n"
+"Return the name of the module for a calling frame.\n"
+"\n"
+"The default depth returns the module containing the call to this API.\n"
+"A more typical use in a library will pass a depth of 1 to get the user\'s\n"
+"module rather than the library module.\n"
+"\n"
+"If no frame, module, or name can be found, returns None.");
+
+#define SYS__GETFRAMEMODULENAME_METHODDEF    \
+    {"_getframemodulename", _PyCFunction_CAST(sys__getframemodulename), METH_FASTCALL|METH_KEYWORDS, sys__getframemodulename__doc__},
+
+static PyObject *
+sys__getframemodulename_impl(PyObject *module, int depth);
+
+static PyObject *
+sys__getframemodulename(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+    #define NUM_KEYWORDS 1
+    static struct {
+        PyGC_Head _this_is_not_used;
+        PyObject_VAR_HEAD
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_item = { &_Py_ID(depth), },
+    };
+    #undef NUM_KEYWORDS
+    #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+    #else  // !Py_BUILD_CORE
+    #  define KWTUPLE NULL
+    #endif  // !Py_BUILD_CORE
+
+    static const char * const _keywords[] = {"depth", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "_getframemodulename",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[1];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0;
+    int depth = 0;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 0, 1, 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    depth = _PyLong_AsInt(args[0]);
+    if (depth == -1 && PyErr_Occurred()) {
+        goto exit;
+    }
+skip_optional_pos:
+    return_value = sys__getframemodulename_impl(module, depth);
+
+exit:
+    return return_value;
+}
+
 #ifndef SYS_GETWINDOWSVERSION_METHODDEF
     #define SYS_GETWINDOWSVERSION_METHODDEF
 #endif /* !defined(SYS_GETWINDOWSVERSION_METHODDEF) */
@@ -1318,4 +1387,4 @@ sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored))
 #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
     #define SYS_GETANDROIDAPILEVEL_METHODDEF
 #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
-/*[clinic end generated code: output=b32b444538dfd354 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=5c761f14326ced54 input=a9049054013a1b77]*/
index acee794864f916fcc443d98720222dc4dd488791..f9f766a94d1464f655e03733825438441e636536 100644 (file)
@@ -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
@@ -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