Skip to content
Open
21 changes: 21 additions & 0 deletions Doc/using/configure.rst
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,27 @@ 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 missing standard library
modules that are packaged separately.

The JSON file should map missing module names to custom error message strings.
For example, a configuration for the :mod:`tkinter` module:

.. code-block:: json

{
"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.
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ Improved error messages
AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'?


* 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 missing
:term:`standard library` modules.
(Contributed by Stan Ulbrych and Petr Viktorin in :gh:`139707`.)


Other language changes
======================

Expand Down
13 changes: 12 additions & 1 deletion Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand All @@ -5061,6 +5061,17 @@ 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)


class TestColorizedTraceback(unittest.TestCase):
maxDiff = None
Expand Down
14 changes: 13 additions & 1 deletion Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

from contextlib import suppress

try:
from _stdlib_modules_info import MISSING_STDLIB_MODULE_MESSAGES
except ImportError:
MISSING_STDLIB_MODULE_MESSAGES = None

__all__ = ['extract_stack', 'extract_tb', 'format_exception',
'format_exception_only', 'format_list', 'format_stack',
'format_tb', 'print_exc', 'format_exc', 'print_exception',
Expand Down Expand Up @@ -1110,7 +1115,14 @@ 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"
if MISSING_STDLIB_MODULE_MESSAGES is not None:
message = MISSING_STDLIB_MODULE_MESSAGES.get(
module_name,
f"Standard library module '{module_name}' was not found"
)
self._str = message
else:
self._str = f"Standard library module '{module_name}' was not found"
elif sys.flags.no_site:
self._str += (". Site initialization is disabled, did you forget to "
+ "add the site-packages directory to sys.path "
Expand Down
6 changes: 6 additions & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,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-stdlib-info="@MISSING_STDLIB_CONFIG@"; \
else \
$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/check_extension_modules.py --generate-stdlib-info; \
fi
@$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/check_extension_modules.py

.PHONY: rundsymutil
Expand Down Expand Up @@ -2815,6 +2820,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`/_stdlib_modules_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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
56 changes: 56 additions & 0 deletions Tools/build/check_extension_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import _imp
import argparse
import enum
import json
import logging
import os
import pathlib
Expand Down Expand Up @@ -116,6 +117,13 @@
help="Print a list of module names to stdout and exit",
)

parser.add_argument(
"--generate-stdlib-info",
nargs="?",
const=True,
help="Generate file with stdlib module info, with optional config file",
)


@enum.unique
class ModuleState(enum.Enum):
Expand Down Expand Up @@ -281,6 +289,50 @@ def list_module_names(self, *, all: bool = False) -> set[str]:
names.update(WINDOWS_MODULES)
return names

def generate_stdlib_info(self, config_path: str | None = None) -> None:

disabled_modules = {modinfo.name for modinfo in self.modules
if modinfo.state in (ModuleState.DISABLED, ModuleState.DISABLED_SETUP)}
missing_modules = {modinfo.name for modinfo in self.modules
if modinfo.state == ModuleState.MISSING}
na_modules = {modinfo.name for modinfo in self.modules
if modinfo.state == ModuleState.NA}

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:
logger.error("Failed to load distributor config %s: %s", config_path, e)

default_messages = {
**{name: f"Windows-only standard library module '{name}' was not found"
for name in WINDOWS_MODULES},
**{name: f"Standard library module disabled during build '{name}' was not found"
for name in disabled_modules},
**{name: f"Unsupported platform for standard library module '{name}'"
for name in na_modules},
}

messages = {**default_messages, **config_messages}

content = f'''\
# Standard library information used by the traceback module for more informative
# ModuleNotFound error messages.

DISABLED_MODULES = {sorted(disabled_modules)!r}
MISSING_MODULES = {sorted(missing_modules)!r}
NOT_AVAILABLE_MODULES = {sorted(na_modules)!r}
WINDOWS_ONLY_MODULES = {sorted(WINDOWS_MODULES)!r}

MISSING_STDLIB_MODULE_MESSAGES = {messages!r}
'''

output_path = self.builddir / "_stdlib_modules_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:
Expand Down Expand Up @@ -499,6 +551,10 @@ def main() -> None:
names = checker.list_module_names(all=True)
for name in sorted(names):
print(name)
elif args.generate_stdlib_info:
checker.check()
config_path = None if args.generate_stdlib_info is True else args.generate_stdlib_info
checker.generate_stdlib_info(config_path)
else:
checker.check()
checker.summary(verbose=args.verbose)
Expand Down
18 changes: 18 additions & 0 deletions configure

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
Loading