Skip to content
Merged
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.
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

The JSON file should map missing module names to custom error message strings.
For example, a configuration for the :mod:`tkinter` module:
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

.. 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
Comment thread
StanFromIreland marked this conversation as resolved.
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
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

__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}
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

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)
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

default_messages = {
**{name: f"Windows-only standard library module '{name}' was not found"
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated
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'''\
Comment thread
StanFromIreland marked this conversation as resolved.
# 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}
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated

MISSING_STDLIB_MODULE_MESSAGES = {messages!r}
Comment thread
StanFromIreland marked this conversation as resolved.
Outdated
'''

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],
Comment thread
StanFromIreland marked this conversation as resolved.
[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