From b5d4347950399800c6703736d716f08761b29245 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 13 Jan 2023 11:31:06 +0000 Subject: [PATCH] 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. --- Doc/library/sys.rst | 16 +++++ Lib/_pydecimal.py | 2 +- Lib/collections/__init__.py | 9 ++- Lib/doctest.py | 8 ++- Lib/enum.py | 12 ++-- Lib/test/audit-tests.py | 11 +++ Lib/test/test_audit.py | 12 ++++ Lib/test/test_sys.py | 20 ++++++ Lib/typing.py | 8 ++- ...2-11-15-23-30-39.gh-issue-86682.gK9i1N.rst | 2 + Objects/funcobject.c | 5 +- Python/clinic/sysmodule.c.h | 71 ++++++++++++++++++- Python/sysmodule.c | 38 ++++++++++ 13 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 28adca1f618d75..605e2c9a6710c1 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -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:: diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index f9d6c9901f1f31..2692f2fcba45bf 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -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 diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index f07ee143a5aff1..b5e4d16e9dbcad 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._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 diff --git a/Lib/doctest.py b/Lib/doctest.py index dafad505ef0e86..2776d74bf9b586 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -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") diff --git a/Lib/enum.py b/Lib/enum.py index 21f63881d78d4d..4658393d756e07 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -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: diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index bf56cea541d121..0edc9d9c472766 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -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 diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index 70f8a77a4761a7..0b69864751d83d 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -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") diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 232b79971dc2b7..ab1a0659471857 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -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() diff --git a/Lib/typing.py b/Lib/typing.py index 8bc38f98c86754..4675af12d087b6 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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 index 00000000000000..64ef42a9a1c0b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-11-15-23-30-39.gh-issue-86682.gK9i1N.rst @@ -0,0 +1,2 @@ +Ensure runtime-created collections have the correct module name using +the newly added (internal) :func:`sys._getframemodulename`. diff --git a/Objects/funcobject.c b/Objects/funcobject.c index d5cf5b9277b3f1..baa360381a7724 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -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; diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index 03eeda8126ebbb..46252dd404325b 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -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]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index acee794864f916..f9f766a94d1464 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -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