From d044be4dc523f72da247cf16a0f0f0294cbfcbe0 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 15 Nov 2022 23:33:42 +0000 Subject: [PATCH] gh-86682: Adds sys._get_calling_module_name and replaces _getframe calls in collections, doctest, enum, and typing modules --- Lib/collections/__init__.py | 9 ++- Lib/doctest.py | 5 +- Lib/enum.py | 10 ++- Lib/test/test_sys.py | 8 +++ Lib/typing.py | 8 ++- ...2-11-15-23-30-39.gh-issue-86682.gK9i1N.rst | 2 + Python/clinic/sysmodule.c.h | 69 ++++++++++++++++++- Python/sysmodule.c | 33 +++++++++ 8 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index f07ee143a5aff1..f0a82b3b1e47aa 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -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._get_calling_module_name(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 diff --git a/Lib/doctest.py b/Lib/doctest.py index b2ef2ce63672eb..7c427eb71c1430 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -207,7 +207,10 @@ 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: + return sys.modules[sys._get_calling_module_name(depth)] + except AttributeError: + return sys.modules[sys._getframe(depth).f_globals['__name__']] else: raise TypeError("Expected a module, string, or None") diff --git a/Lib/enum.py b/Lib/enum.py index 1b683c702d59b4..d9d746fa62c0e5 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -864,9 +864,13 @@ 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__'] - except (AttributeError, ValueError, KeyError): - pass + module = sys._get_calling_module_name(2) + except AttributeError: + # Fall back on _getframe if _get_calling_module_name is missing + try: + module = sys._getframe(2).f_globals['__name__'] + except (AttributeError, ValueError, KeyError): + pass if module is None: _make_class_unpicklable(classdict) else: diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 2403c7c815f2c0..d5e855d32044e4 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -399,6 +399,14 @@ def test_getframe(self): is sys._getframe().f_code ) + def test_get_calling_module_name(self): + # Default depth gets ourselves + self.assertEqual("test.test_sys", sys._get_calling_module_name()) + # Get our caller + self.assertEqual("unittest.case", sys._get_calling_module_name(1)) + # Get our caller's caller's caller's caller + self.assertEqual("unittest.suite", sys._get_calling_module_name(4)) + # sys._current_frames() is a CPython-only gimmick. @threading_helper.reap_threads @threading_helper.requires_working_threading() diff --git a/Lib/typing.py b/Lib/typing.py index 233941598f76a3..64d1d814faa596 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1942,11 +1942,15 @@ def _no_init_or_replace_init(self, *args, **kwargs): def _caller(depth=1, default='__main__'): + try: + return sys._get_calling_module_name(depth + 1) or default + except AttributeError: # For platforms without _get_calling_module_name() + 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 index 00000000000000..8b244286f37cbf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst @@ -0,0 +1,2 @@ +Changes internal function used to ensure runtime-created collections have +the correct module name from ``_getframe`` to ``_get_calling_module_name``. diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 5678d0ac2a608b..f92cf2e036314a 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1275,6 +1275,73 @@ sys_is_stack_trampoline_active(PyObject *module, PyObject *Py_UNUSED(ignored)) return sys_is_stack_trampoline_active_impl(module); } +PyDoc_STRVAR(sys__get_calling_module_name__doc__, +"_get_calling_module_name($module, /, depth=0)\n" +"--\n" +"\n" +"Return the name of the calling module.\n" +"\n" +"The default depth returns the module containing the call to this function.\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."); + +#define SYS__GET_CALLING_MODULE_NAME_METHODDEF \ + {"_get_calling_module_name", _PyCFunction_CAST(sys__get_calling_module_name), METH_FASTCALL|METH_KEYWORDS, sys__get_calling_module_name__doc__}, + +static PyObject * +sys__get_calling_module_name_impl(PyObject *module, int depth); + +static PyObject * +sys__get_calling_module_name(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 = "_get_calling_module_name", + .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__get_calling_module_name_impl(module, depth); + +exit: + return return_value; +} + #ifndef SYS_GETWINDOWSVERSION_METHODDEF #define SYS_GETWINDOWSVERSION_METHODDEF #endif /* !defined(SYS_GETWINDOWSVERSION_METHODDEF) */ @@ -1318,4 +1385,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=79228e569529129c input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e5518d74e7578a25 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 6f0a126a62277b..6a5291202ce32e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2180,6 +2180,38 @@ sys_is_stack_trampoline_active_impl(PyObject *module) } +/*[clinic input] +sys._get_calling_module_name + + depth: int = 0 + +Return the name of the calling module. + +The default depth returns the module containing the call to this function. +A more typical use in a library will pass a depth of 1 to get the user's +module rather than the library module. +[clinic start generated code]*/ + +static PyObject * +sys__get_calling_module_name_impl(PyObject *module, int depth) +/*[clinic end generated code: output=bd04c211226f8b84 input=dd268ae6a20311ab]*/ +{ + _PyInterpreterFrame *f = _PyThreadState_GET()->cframe->current_frame; + while (f && (f->owner == FRAME_OWNED_BY_CSTACK || depth-- > 0)) { + f = f->previous; + } + if (f == NULL) { + Py_RETURN_NONE; + } + PyObject *r = PyDict_GetItemWithError(f->f_globals, &_Py_ID(__name__)); + 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 @@ -2197,6 +2229,7 @@ static PyMethodDef sys_methods[] = { SYS_GETDEFAULTENCODING_METHODDEF SYS_GETDLOPENFLAGS_METHODDEF SYS_GETALLOCATEDBLOCKS_METHODDEF + SYS__GET_CALLING_MODULE_NAME_METHODDEF SYS_GETFILESYSTEMENCODING_METHODDEF SYS_GETFILESYSTEMENCODEERRORS_METHODDEF #ifdef Py_TRACE_REFS