https://github.com/python/cpython/commit/d4fa70706c95a5eec4cca340c6232c92168f6cff
commit: d4fa70706c95a5eec4cca340c6232c92168f6cff
branch: main
author: Stan Ulbrych <[email protected]>
committer: encukou <[email protected]>
date: 2025-12-01T14:36:17+01:00
summary:

gh-139707: Add mechanism for distributors to supply error messages for missing 
stdlib modules (GH-140783)

files:
A Misc/NEWS.d/next/Build/2025-10-30-10-36-15.gh-issue-139707.QJ1FfJ.rst
M Doc/using/configure.rst
M Doc/whatsnew/3.15.rst
M Lib/test/test_traceback.py
M Lib/traceback.py
M Makefile.pre.in
M Tools/build/check_extension_modules.py
M configure
M configure.ac

diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst
index cdadbe51417499..e140ca5d71f555 100644
--- a/Doc/using/configure.rst
+++ b/Doc/using/configure.rst
@@ -322,6 +322,30 @@ General Options
 
    .. versionadded:: 3.11
 
+.. option:: --with-missing-stdlib-config=FILE
+
+   Path to a `JSON <https://www.json.org/json-en.html>`_ configuration file
+   containing custom error messages for missing :term:`standard library` 
modules.
+
+   This option is intended for Python distributors who wish to provide
+   distribution-specific guidance when users encounter standard library
+   modules that are missing or packaged separately.
+
+   The JSON file should map missing module names to custom error message 
strings.
+   For example, if your distribution packages :mod:`tkinter` and
+   :mod:`_tkinter` separately and excludes :mod:`!_gdbm` for legal reasons,
+   the configuration could contain:
+
+   .. code-block:: json
+
+      {
+          "_gdbm": "The '_gdbm' module is not available in this distribution"
+          "tkinter": "Install the python-tk package to use tkinter",
+          "_tkinter": "Install the python-tk package to use tkinter",
+      }
+
+   .. versionadded:: next
+
 .. option:: --enable-pystats
 
    Turn on internal Python performance statistics gathering.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 4882ddb4310fc2..27e3f23e47c875 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1247,6 +1247,12 @@ Build changes
   set to ``no`` or with :option:`!--without-system-libmpdec`.
   (Contributed by Sergey B Kirpichev in :gh:`115119`.)
 
+* The new configure option :option:`--with-missing-stdlib-config=FILE` allows
+  distributors to pass a `JSON <https://www.json.org/json-en.html>`_
+  configuration file containing custom error messages for :term:`standard 
library`
+  modules that are missing or packaged separately.
+  (Contributed by Stan Ulbrych and Petr Viktorin in :gh:`139707`.)
+
 
 Porting to Python 3.15
 ======================
diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index bf57867a8715c0..3876f1a74bbc1a 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -5051,7 +5051,7 @@ def test_no_site_package_flavour(self):
              b"or to enable your virtual environment?"), stderr
         )
 
-    def test_missing_stdlib_package(self):
+    def test_missing_stdlib_module(self):
         code = """
             import sys
             sys.stdlib_module_names |= {'spam'}
@@ -5061,6 +5061,27 @@ def test_missing_stdlib_package(self):
 
         self.assertIn(b"Standard library module 'spam' was not found", stderr)
 
+        code = """
+            import sys
+            import traceback
+            traceback._MISSING_STDLIB_MODULE_MESSAGES = {'spam': "Install 
'spam4life' for 'spam'"}
+            sys.stdlib_module_names |= {'spam'}
+            import spam
+        """
+        _, _, stderr = assert_python_failure('-S', '-c', code)
+
+        self.assertIn(b"Install 'spam4life' for 'spam'", stderr)
+
+    @unittest.skipIf(sys.platform == "win32", "Non-Windows test")
+    def test_windows_only_module_error(self):
+        try:
+            import msvcrt  # noqa: F401
+        except ModuleNotFoundError:
+            formatted = traceback.format_exc()
+            self.assertIn("Unsupported platform for Windows-only standard 
library module 'msvcrt'", formatted)
+        else:
+            self.fail("ModuleNotFoundError was not raised")
+
 
 class TestColorizedTraceback(unittest.TestCase):
     maxDiff = None
diff --git a/Lib/traceback.py b/Lib/traceback.py
index 9b4b8c7d566fe8..8a3e0f77e765dc 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -14,6 +14,11 @@
 
 from contextlib import suppress
 
+try:
+    from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES
+except ImportError:
+    _MISSING_STDLIB_MODULE_MESSAGES = {}
+
 __all__ = ['extract_stack', 'extract_tb', 'format_exception',
            'format_exception_only', 'format_list', 'format_stack',
            'format_tb', 'print_exc', 'format_exc', 'print_exception',
@@ -1110,7 +1115,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, 
*, limit=None,
         elif exc_type and issubclass(exc_type, ModuleNotFoundError):
             module_name = getattr(exc_value, "name", None)
             if module_name in sys.stdlib_module_names:
-                self._str = f"Standard library module '{module_name}' was not 
found"
+                message = _MISSING_STDLIB_MODULE_MESSAGES.get(
+                    module_name,
+                    f"Standard library module {module_name!r} was not found"
+                )
+                self._str = message
             elif sys.flags.no_site:
                 self._str += (". Site initialization is disabled, did you 
forget to "
                     + "add the site-packages directory to sys.path "
diff --git a/Makefile.pre.in b/Makefile.pre.in
index 7b8e7ec0965180..816080faa1f5c3 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1604,6 +1604,11 @@ sharedmods: $(SHAREDMODS) pybuilddir.txt
 # dependency on BUILDPYTHON ensures that the target is run last
 .PHONY: checksharedmods
 checksharedmods: sharedmods $(PYTHON_FOR_BUILD_DEPS) $(BUILDPYTHON)
+       @if [ -n "@MISSING_STDLIB_CONFIG@" ]; then \
+               $(RUNSHARED) $(PYTHON_FOR_BUILD) 
$(srcdir)/Tools/build/check_extension_modules.py --generate-missing-stdlib-info 
--with-missing-stdlib-config="@MISSING_STDLIB_CONFIG@"; \
+       else \
+               $(RUNSHARED) $(PYTHON_FOR_BUILD) 
$(srcdir)/Tools/build/check_extension_modules.py 
--generate-missing-stdlib-info; \
+       fi
        @$(RUNSHARED) $(PYTHON_FOR_BUILD) 
$(srcdir)/Tools/build/check_extension_modules.py
 
 .PHONY: rundsymutil
@@ -2820,6 +2825,7 @@ libinstall:       all $(srcdir)/Modules/xxmodule.c
        $(INSTALL_DATA) `cat 
pybuilddir.txt`/_sysconfigdata_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).py 
$(DESTDIR)$(LIBDEST); \
        $(INSTALL_DATA) `cat 
pybuilddir.txt`/_sysconfig_vars_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).json 
$(DESTDIR)$(LIBDEST); \
        $(INSTALL_DATA) `cat pybuilddir.txt`/build-details.json 
$(DESTDIR)$(LIBDEST); \
+       $(INSTALL_DATA) `cat pybuilddir.txt`/_missing_stdlib_info.py 
$(DESTDIR)$(LIBDEST); \
        $(INSTALL_DATA) $(srcdir)/LICENSE $(DESTDIR)$(LIBDEST)/LICENSE.txt
        @ # If app store compliance has been configured, apply the patch to the
        @ # installed library code. The patch has been previously validated 
against
diff --git 
a/Misc/NEWS.d/next/Build/2025-10-30-10-36-15.gh-issue-139707.QJ1FfJ.rst 
b/Misc/NEWS.d/next/Build/2025-10-30-10-36-15.gh-issue-139707.QJ1FfJ.rst
new file mode 100644
index 00000000000000..d9870d267042af
--- /dev/null
+++ b/Misc/NEWS.d/next/Build/2025-10-30-10-36-15.gh-issue-139707.QJ1FfJ.rst
@@ -0,0 +1,4 @@
+Add configure option :option:`--with-missing-stdlib-config=FILE` allows
+which distributors to pass a `JSON <https://www.json.org/json-en.html>`_
+configuration file containing custom error messages for missing
+:term:`standard library` modules.
diff --git a/Tools/build/check_extension_modules.py 
b/Tools/build/check_extension_modules.py
index 668db8df0bd181..f23c1d5286f92a 100644
--- a/Tools/build/check_extension_modules.py
+++ b/Tools/build/check_extension_modules.py
@@ -23,9 +23,11 @@
 import _imp
 import argparse
 import enum
+import json
 import logging
 import os
 import pathlib
+import pprint
 import re
 import sys
 import sysconfig
@@ -116,6 +118,18 @@
     help="Print a list of module names to stdout and exit",
 )
 
+parser.add_argument(
+    "--generate-missing-stdlib-info",
+    action="store_true",
+    help="Generate file with stdlib module info",
+)
+
+parser.add_argument(
+    "--with-missing-stdlib-config",
+    metavar="CONFIG_FILE",
+    help="Path to JSON config file with custom missing module messages",
+)
+
 
 @enum.unique
 class ModuleState(enum.Enum):
@@ -281,6 +295,39 @@ def list_module_names(self, *, all: bool = False) -> 
set[str]:
             names.update(WINDOWS_MODULES)
         return names
 
+    def generate_missing_stdlib_info(self, config_path: str | None = None) -> 
None:
+        config_messages = {}
+        if config_path:
+            try:
+                with open(config_path, encoding='utf-8') as f:
+                    config_messages = json.load(f)
+            except (FileNotFoundError, json.JSONDecodeError) as e:
+                raise RuntimeError(f"Failed to load missing stdlib config 
{config_path!r}") from e
+
+        messages = {}
+        for name in WINDOWS_MODULES:
+            messages[name] = f"Unsupported platform for Windows-only standard 
library module {name!r}"
+
+        for modinfo in self.modules:
+            if modinfo.state in (ModuleState.DISABLED, 
ModuleState.DISABLED_SETUP):
+                messages[modinfo.name] = f"Standard library module disabled 
during build {modinfo.name!r} was not found"
+            elif modinfo.state == ModuleState.NA:
+                messages[modinfo.name] = f"Unsupported platform for standard 
library module {modinfo.name!r}"
+
+        messages.update(config_messages)
+
+        content = f'''\
+# Standard library information used by the traceback module for more 
informative
+# ModuleNotFound error messages.
+# Generated by check_extension_modules.py
+
+_MISSING_STDLIB_MODULE_MESSAGES = {pprint.pformat(messages)}
+'''
+
+        output_path = self.builddir / "_missing_stdlib_info.py"
+        with open(output_path, "w", encoding="utf-8") as f:
+            f.write(content)
+
     def get_builddir(self) -> pathlib.Path:
         try:
             with open(self.pybuilddir_txt, encoding="utf-8") as f:
@@ -499,6 +546,9 @@ def main() -> None:
         names = checker.list_module_names(all=True)
         for name in sorted(names):
             print(name)
+    elif args.generate_missing_stdlib_info:
+        checker.check()
+        checker.generate_missing_stdlib_info(args.with_missing_stdlib_config)
     else:
         checker.check()
         checker.summary(verbose=args.verbose)
diff --git a/configure b/configure
index 4bcb639d781dd7..620878bb181378 100755
--- a/configure
+++ b/configure
@@ -1012,6 +1012,7 @@ UNIVERSALSDK
 host_exec_prefix
 host_prefix
 MACHDEP
+MISSING_STDLIB_CONFIG
 PKG_CONFIG_LIBDIR
 PKG_CONFIG_PATH
 PKG_CONFIG
@@ -1083,6 +1084,7 @@ ac_user_opts='
 enable_option_checking
 with_build_python
 with_pkg_config
+with_missing_stdlib_config
 enable_universalsdk
 with_universal_archs
 with_framework_name
@@ -1862,6 +1864,9 @@ Optional Packages:
   --with-pkg-config=[yes|no|check]
                           use pkg-config to detect build options (default is
                           check)
+  --with-missing-stdlib-config=FILE
+                          File with custom module error messages for missing
+                          stdlib modules
   --with-universal-archs=ARCH
                           specify the kind of macOS universal binary that
                           should be created. This option is only valid when
@@ -4095,6 +4100,19 @@ if test "$with_pkg_config" = yes -a -z "$PKG_CONFIG"; 
then
   as_fn_error $? "pkg-config is required" "$LINENO" 5]
 fi
 
+
+# Check whether --with-missing-stdlib-config was given.
+if test ${with_missing_stdlib_config+y}
+then :
+  withval=$with_missing_stdlib_config; MISSING_STDLIB_CONFIG="$withval"
+else case e in #(
+  e) MISSING_STDLIB_CONFIG=""
+ ;;
+esac
+fi
+
+
+
 # Set name for machine-dependent library files
 
 { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking MACHDEP" >&5
diff --git a/configure.ac b/configure.ac
index a1f1cf207c5f34..8ef479fe32036c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -307,6 +307,15 @@ if test "$with_pkg_config" = yes -a -z "$PKG_CONFIG"; then
   AC_MSG_ERROR([pkg-config is required])]
 fi
 
+dnl Allow distributors to provide custom missing stdlib module error messages
+AC_ARG_WITH([missing-stdlib-config],
+  [AS_HELP_STRING([--with-missing-stdlib-config=FILE],
+                  [File with custom module error messages for missing stdlib 
modules])],
+  [MISSING_STDLIB_CONFIG="$withval"],
+  [MISSING_STDLIB_CONFIG=""]
+)
+AC_SUBST([MISSING_STDLIB_CONFIG])
+
 # Set name for machine-dependent library files
 AC_ARG_VAR([MACHDEP], [name for machine-dependent library files])
 AC_MSG_CHECKING([MACHDEP])

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to