gh-139707: Add mechanism for distributors to supply error messages for missing stdlib modules (GH-140783)
Some checks are pending
Tests / Change detection (push) Waiting to run
Tests / Docs (push) Blocked by required conditions
Tests / (push) Blocked by required conditions
Tests / Windows MSI (push) Blocked by required conditions
Tests / WASI (push) Blocked by required conditions
Tests / Check if Autoconf files are up to date (push) Blocked by required conditions
Tests / Check if generated files are up to date (push) Blocked by required conditions
Tests / Hypothesis tests on Ubuntu (push) Blocked by required conditions
Tests / Ubuntu SSL tests with OpenSSL (push) Blocked by required conditions
Tests / Ubuntu SSL tests with AWS-LC (push) Blocked by required conditions
Tests / Android (aarch64) (push) Blocked by required conditions
Tests / Android (x86_64) (push) Blocked by required conditions
Tests / iOS (push) Blocked by required conditions
Tests / Address sanitizer (push) Blocked by required conditions
Tests / Sanitizers (push) Blocked by required conditions
Tests / Cross build Linux (push) Blocked by required conditions
Tests / CIFuzz (push) Blocked by required conditions
Tests / All required checks pass (push) Blocked by required conditions
Lint / lint (push) Waiting to run
mypy / Run mypy on Lib/_pyrepl (push) Waiting to run
mypy / Run mypy on Lib/test/libregrtest (push) Waiting to run
mypy / Run mypy on Lib/tomllib (push) Waiting to run
mypy / Run mypy on Tools/build (push) Waiting to run
mypy / Run mypy on Tools/cases_generator (push) Waiting to run
mypy / Run mypy on Tools/clinic (push) Waiting to run
mypy / Run mypy on Tools/jit (push) Waiting to run
mypy / Run mypy on Tools/peg_generator (push) Waiting to run

This commit is contained in:
Stan Ulbrych 2025-12-01 13:36:17 +00:00 committed by GitHub
parent b708485d1a
commit d4fa70706c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 149 additions and 2 deletions

View 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.

View 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
======================

View 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

View 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 "

View 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

View file

@ -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.

View 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)

18
configure generated vendored
View file

@ -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

View 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])