Add _showwarnmsg() and _formatwarnmsg() to warnings

Issue #26568: add new  _showwarnmsg() and _formatwarnmsg() functions to the
warnings module.

The C function warn_explicit() now calls warnings._showwarnmsg() with a
warnings.WarningMessage as parameter, instead of calling warnings.showwarning()
with multiple parameters.

_showwarnmsg() calls warnings.showwarning() if warnings.showwarning() was
replaced. Same for _formatwarnmsg(): call warnings.formatwarning() if it was
replaced.
diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py
index cea9c57..70eae4c 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -651,6 +651,17 @@
                 result = stream.getvalue()
         self.assertIn(text, result)
 
+    def test_showwarnmsg_missing(self):
+        # Test that _showwarnmsg() missing is okay.
+        text = 'del _showwarnmsg test'
+        with original_warnings.catch_warnings(module=self.module):
+            self.module.filterwarnings("always", category=UserWarning)
+            del self.module._showwarnmsg
+            with support.captured_output('stderr') as stream:
+                self.module.warn(text)
+                result = stream.getvalue()
+        self.assertIn(text, result)
+
     def test_showwarning_not_callable(self):
         with original_warnings.catch_warnings(module=self.module):
             self.module.filterwarnings("always", category=UserWarning)
diff --git a/Lib/warnings.py b/Lib/warnings.py
index 1d4fb20..f54726a 100644
--- a/Lib/warnings.py
+++ b/Lib/warnings.py
@@ -6,24 +6,63 @@
            "formatwarning", "filterwarnings", "simplefilter",
            "resetwarnings", "catch_warnings"]
 
-
 def showwarning(message, category, filename, lineno, file=None, line=None):
     """Hook to write a warning to a file; replace if you like."""
-    if file is None:
-        file = sys.stderr
-        if file is None:
-            # sys.stderr is None when run with pythonw.exe - warnings get lost
-            return
-    try:
-        file.write(formatwarning(message, category, filename, lineno, line))
-    except OSError:
-        pass # the file (probably stderr) is invalid - this warning gets lost.
+    msg = WarningMessage(message, category, filename, lineno, file, line)
+    _showwarnmsg(msg)
 
 def formatwarning(message, category, filename, lineno, line=None):
     """Function to format a warning the standard way."""
+    msg = WarningMessage(message, category, filename, lineno, None, line)
+    return _formatwarnmsg(msg)
+
+# Keep references to check if the functions were replaced
+_showwarning = showwarning
+_formatwarning = formatwarning
+
+def _showwarnmsg(msg):
+    """Hook to write a warning to a file; replace if you like."""
+    showwarning = globals().get('showwarning', _showwarning)
+    if showwarning is not _showwarning:
+        # warnings.showwarning() was replaced
+        if not callable(showwarning):
+            raise TypeError("warnings.showwarning() must be set to a "
+                            "function or method")
+
+        showwarning(msg.message, msg.category, msg.filename, msg.lineno,
+                    msg.file, msg.line)
+        return
+
+    file = msg.file
+    if file is None:
+        file = sys.stderr
+        if file is None:
+            # sys.stderr is None when run with pythonw.exe:
+            # warnings get lost
+            return
+    text = _formatwarnmsg(msg)
+    try:
+        file.write(text)
+    except OSError:
+        # the file (probably stderr) is invalid - this warning gets lost.
+        pass
+
+def _formatwarnmsg(msg):
+    """Function to format a warning the standard way."""
+    formatwarning = globals().get('formatwarning', _formatwarning)
+    if formatwarning is not _formatwarning:
+        # warnings.formatwarning() was replaced
+        return formatwarning(msg.message, msg.category,
+                             msg.filename, msg.lineno, line=msg.line)
+
     import linecache
-    s =  "%s:%s: %s: %s\n" % (filename, lineno, category.__name__, message)
-    line = linecache.getline(filename, lineno) if line is None else line
+    s =  ("%s:%s: %s: %s\n"
+          % (msg.filename, msg.lineno, msg.category.__name__,
+             msg.message))
+    if msg.line is None:
+        line = linecache.getline(msg.filename, msg.lineno)
+    else:
+        line = msg.line
     if line:
         line = line.strip()
         s += "  %s\n" % line
@@ -293,17 +332,13 @@
         raise RuntimeError(
               "Unrecognized action (%r) in warnings.filters:\n %s" %
               (action, item))
-    if not callable(showwarning):
-        raise TypeError("warnings.showwarning() must be set to a "
-                        "function or method")
     # Print message and context
-    showwarning(message, category, filename, lineno)
+    msg = WarningMessage(message, category, filename, lineno)
+    _showwarnmsg(msg)
 
 
 class WarningMessage(object):
 
-    """Holds the result of a single showwarning() call."""
-
     _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file",
                         "line")
 
@@ -366,11 +401,12 @@
         self._module.filters = self._filters[:]
         self._module._filters_mutated()
         self._showwarning = self._module.showwarning
+        self._showwarnmsg = self._module._showwarnmsg
         if self._record:
             log = []
-            def showwarning(*args, **kwargs):
-                log.append(WarningMessage(*args, **kwargs))
-            self._module.showwarning = showwarning
+            def showarnmsg(msg):
+                log.append(msg)
+            self._module._showwarnmsg = showarnmsg
             return log
         else:
             return None
@@ -381,6 +417,7 @@
         self._module.filters = self._filters
         self._module._filters_mutated()
         self._module.showwarning = self._showwarning
+        self._module._showwarnmsg = self._showwarnmsg
 
 
 # filters contains a sequence of filter 5-tuples
diff --git a/Python/_warnings.c b/Python/_warnings.c
index daa1355..a8c3703 100644
--- a/Python/_warnings.c
+++ b/Python/_warnings.c
@@ -359,6 +359,56 @@
     PyErr_Clear();
 }
 
+static int
+call_show_warning(PyObject *category, PyObject *text, PyObject *message,
+                  PyObject *filename, int lineno, PyObject *lineno_obj,
+                  PyObject *sourceline)
+{
+    PyObject *show_fn, *msg, *res, *warnmsg_cls = NULL;
+
+    show_fn = get_warnings_attr("_showwarnmsg");
+    if (show_fn == NULL) {
+        if (PyErr_Occurred())
+            return -1;
+        show_warning(filename, lineno, text, category, sourceline);
+        return 0;
+    }
+
+    if (!PyCallable_Check(show_fn)) {
+        PyErr_SetString(PyExc_TypeError,
+                "warnings._showwarnmsg() must be set to a callable");
+        goto error;
+    }
+
+    warnmsg_cls = get_warnings_attr("WarningMessage");
+    if (warnmsg_cls == NULL) {
+        PyErr_SetString(PyExc_RuntimeError,
+                "unable to get warnings.WarningMessage");
+        goto error;
+    }
+
+    msg = PyObject_CallFunctionObjArgs(warnmsg_cls, message, category,
+            filename, lineno_obj,
+            NULL);
+    Py_DECREF(warnmsg_cls);
+    if (msg == NULL)
+        goto error;
+
+    res = PyObject_CallFunctionObjArgs(show_fn, msg, NULL);
+    Py_DECREF(show_fn);
+    Py_DECREF(msg);
+
+    if (res == NULL)
+        return -1;
+
+    Py_DECREF(res);
+    return 0;
+
+error:
+    Py_XDECREF(show_fn);
+    return -1;
+}
+
 static PyObject *
 warn_explicit(PyObject *category, PyObject *message,
               PyObject *filename, int lineno,
@@ -470,31 +520,9 @@
     if (rc == 1)  /* Already warned for this module. */
         goto return_none;
     if (rc == 0) {
-        PyObject *show_fxn = get_warnings_attr("showwarning");
-        if (show_fxn == NULL) {
-            if (PyErr_Occurred())
-                goto cleanup;
-            show_warning(filename, lineno, text, category, sourceline);
-        }
-        else {
-            PyObject *res;
-
-            if (!PyCallable_Check(show_fxn)) {
-                PyErr_SetString(PyExc_TypeError,
-                                "warnings.showwarning() must be set to a "
-                                "callable");
-                Py_DECREF(show_fxn);
-                goto cleanup;
-            }
-
-            res = PyObject_CallFunctionObjArgs(show_fxn, message, category,
-                                                filename, lineno_obj,
-                                                NULL);
-            Py_DECREF(show_fxn);
-            Py_XDECREF(res);
-            if (res == NULL)
-                goto cleanup;
-        }
+        if (call_show_warning(category, text, message, filename, lineno,
+                              lineno_obj, sourceline) < 0)
+            goto cleanup;
     }
     else /* if (rc == -1) */
         goto cleanup;