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]