]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-139707: Add mechanism for distributors to supply error messages for missing stdlib...
authorStan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Mon, 1 Dec 2025 13:36:17 +0000 (13:36 +0000)
committerGitHub <noreply@github.com>
Mon, 1 Dec 2025 13:36:17 +0000 (14:36 +0100)
Doc/using/configure.rst
Doc/whatsnew/3.15.rst
Lib/test/test_traceback.py
Lib/traceback.py
Makefile.pre.in
Misc/NEWS.d/next/Build/2025-10-30-10-36-15.gh-issue-139707.QJ1FfJ.rst [new file with mode: 0644]
Tools/build/check_extension_modules.py
configure
configure.ac

index cdadbe51417499588453edbcb7e5028417c97e77..e140ca5d71f55550d5037b25f4d87f79e93ca2fb 100644 (file)
@@ -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.
index 4882ddb4310fc21b4d0cf37d3f4f1e110bc96b06..27e3f23e47c875d6e3cb39328fbbaed24ca26c7d 100644 (file)
@@ -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
 ======================
index bf57867a8715c043a43f7cbb2a8a01972b15a79f..3876f1a74bbc1ae263a9439da14a163d75e01f5b 100644 (file)
@@ -5051,7 +5051,7 @@ class MiscTest(unittest.TestCase):
              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 @@ class MiscTest(unittest.TestCase):
 
         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
index 9b4b8c7d566fe81845abd6bbe42c76397412d27d..8a3e0f77e765dc61b45764d25522f3e768dd6a7d 100644 (file)
@@ -14,6 +14,11 @@ import _colorize
 
 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 @@ class TracebackException:
         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 "
index 7b8e7ec0965180b896e374995c8c557b89b3f851..816080faa1f5c351943b36ac05ccecd295ae9f0b 100644 (file)
@@ -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 (file)
index 0000000..d9870d2
--- /dev/null
@@ -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.
index 668db8df0bd1812b92fb41a6de01dc86642a5dc8..f23c1d5286f92af0bb573d9034defc24b6b578ec 100644 (file)
@@ -23,9 +23,11 @@ from __future__ import annotations
 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 @@ parser.add_argument(
     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 @@ class ModuleChecker:
             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)
index 4bcb639d781dd79175423b44a7ad85e296615b1c..620878bb1813786e303b532ae8b654aa8628afb4 100755 (executable)
--- 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
index a1f1cf207c5f344dc9d08908f1d7b6cf82e5ddbd..8ef479fe32036cb30bc729ca79e9f290339addde 100644 (file)
@@ -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])