Skip to content

Commit d4fa707

Browse files
gh-139707: Add mechanism for distributors to supply error messages for missing stdlib modules (GH-140783)
1 parent b708485 commit d4fa707

File tree

9 files changed

+149
-2
lines changed

9 files changed

+149
-2
lines changed

Doc/using/configure.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,30 @@ General Options
322322

323323
.. versionadded:: 3.11
324324

325+
.. option:: --with-missing-stdlib-config=FILE
326+
327+
Path to a `JSON <https://www.json.org/json-en.html>`_ configuration file
328+
containing custom error messages for missing :term:`standard library` modules.
329+
330+
This option is intended for Python distributors who wish to provide
331+
distribution-specific guidance when users encounter standard library
332+
modules that are missing or packaged separately.
333+
334+
The JSON file should map missing module names to custom error message strings.
335+
For example, if your distribution packages :mod:`tkinter` and
336+
:mod:`_tkinter` separately and excludes :mod:`!_gdbm` for legal reasons,
337+
the configuration could contain:
338+
339+
.. code-block:: json
340+
341+
{
342+
"_gdbm": "The '_gdbm' module is not available in this distribution"
343+
"tkinter": "Install the python-tk package to use tkinter",
344+
"_tkinter": "Install the python-tk package to use tkinter",
345+
}
346+
347+
.. versionadded:: next
348+
325349
.. option:: --enable-pystats
326350

327351
Turn on internal Python performance statistics gathering.

Doc/whatsnew/3.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,12 @@ Build changes
12471247
set to ``no`` or with :option:`!--without-system-libmpdec`.
12481248
(Contributed by Sergey B Kirpichev in :gh:`115119`.)
12491249

1250+
* The new configure option :option:`--with-missing-stdlib-config=FILE` allows
1251+
distributors to pass a `JSON <https://www.json.org/json-en.html>`_
1252+
configuration file containing custom error messages for :term:`standard library`
1253+
modules that are missing or packaged separately.
1254+
(Contributed by Stan Ulbrych and Petr Viktorin in :gh:`139707`.)
1255+
12501256

12511257
Porting to Python 3.15
12521258
======================

Lib/test/test_traceback.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5051,7 +5051,7 @@ def test_no_site_package_flavour(self):
50515051
b"or to enable your virtual environment?"), stderr
50525052
)
50535053

5054-
def test_missing_stdlib_package(self):
5054+
def test_missing_stdlib_module(self):
50555055
code = """
50565056
import sys
50575057
sys.stdlib_module_names |= {'spam'}
@@ -5061,6 +5061,27 @@ def test_missing_stdlib_package(self):
50615061

50625062
self.assertIn(b"Standard library module 'spam' was not found", stderr)
50635063

5064+
code = """
5065+
import sys
5066+
import traceback
5067+
traceback._MISSING_STDLIB_MODULE_MESSAGES = {'spam': "Install 'spam4life' for 'spam'"}
5068+
sys.stdlib_module_names |= {'spam'}
5069+
import spam
5070+
"""
5071+
_, _, stderr = assert_python_failure('-S', '-c', code)
5072+
5073+
self.assertIn(b"Install 'spam4life' for 'spam'", stderr)
5074+
5075+
@unittest.skipIf(sys.platform == "win32", "Non-Windows test")
5076+
def test_windows_only_module_error(self):
5077+
try:
5078+
import msvcrt # noqa: F401
5079+
except ModuleNotFoundError:
5080+
formatted = traceback.format_exc()
5081+
self.assertIn("Unsupported platform for Windows-only standard library module 'msvcrt'", formatted)
5082+
else:
5083+
self.fail("ModuleNotFoundError was not raised")
5084+
50645085

50655086
class TestColorizedTraceback(unittest.TestCase):
50665087
maxDiff = None

Lib/traceback.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414

1515
from contextlib import suppress
1616

17+
try:
18+
from _missing_stdlib_info import _MISSING_STDLIB_MODULE_MESSAGES
19+
except ImportError:
20+
_MISSING_STDLIB_MODULE_MESSAGES = {}
21+
1722
__all__ = ['extract_stack', 'extract_tb', 'format_exception',
1823
'format_exception_only', 'format_list', 'format_stack',
1924
'format_tb', 'print_exc', 'format_exc', 'print_exception',
@@ -1110,7 +1115,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11101115
elif exc_type and issubclass(exc_type, ModuleNotFoundError):
11111116
module_name = getattr(exc_value, "name", None)
11121117
if module_name in sys.stdlib_module_names:
1113-
self._str = f"Standard library module '{module_name}' was not found"
1118+
message = _MISSING_STDLIB_MODULE_MESSAGES.get(
1119+
module_name,
1120+
f"Standard library module {module_name!r} was not found"
1121+
)
1122+
self._str = message
11141123
elif sys.flags.no_site:
11151124
self._str += (". Site initialization is disabled, did you forget to "
11161125
+ "add the site-packages directory to sys.path "

Makefile.pre.in

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,11 @@ sharedmods: $(SHAREDMODS) pybuilddir.txt
16041604
# dependency on BUILDPYTHON ensures that the target is run last
16051605
.PHONY: checksharedmods
16061606
checksharedmods: sharedmods $(PYTHON_FOR_BUILD_DEPS) $(BUILDPYTHON)
1607+
@if [ -n "@MISSING_STDLIB_CONFIG@" ]; then \
1608+
$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/check_extension_modules.py --generate-missing-stdlib-info --with-missing-stdlib-config="@MISSING_STDLIB_CONFIG@"; \
1609+
else \
1610+
$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/check_extension_modules.py --generate-missing-stdlib-info; \
1611+
fi
16071612
@$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/build/check_extension_modules.py
16081613

16091614
.PHONY: rundsymutil
@@ -2820,6 +2825,7 @@ libinstall: all $(srcdir)/Modules/xxmodule.c
28202825
$(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfigdata_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).py $(DESTDIR)$(LIBDEST); \
28212826
$(INSTALL_DATA) `cat pybuilddir.txt`/_sysconfig_vars_$(ABIFLAGS)_$(MACHDEP)_$(MULTIARCH).json $(DESTDIR)$(LIBDEST); \
28222827
$(INSTALL_DATA) `cat pybuilddir.txt`/build-details.json $(DESTDIR)$(LIBDEST); \
2828+
$(INSTALL_DATA) `cat pybuilddir.txt`/_missing_stdlib_info.py $(DESTDIR)$(LIBDEST); \
28232829
$(INSTALL_DATA) $(srcdir)/LICENSE $(DESTDIR)$(LIBDEST)/LICENSE.txt
28242830
@ # If app store compliance has been configured, apply the patch to the
28252831
@ # installed library code. The patch has been previously validated against
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add configure option :option:`--with-missing-stdlib-config=FILE` allows
2+
which distributors to pass a `JSON <https://www.json.org/json-en.html>`_
3+
configuration file containing custom error messages for missing
4+
:term:`standard library` modules.

Tools/build/check_extension_modules.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
import _imp
2424
import argparse
2525
import enum
26+
import json
2627
import logging
2728
import os
2829
import pathlib
30+
import pprint
2931
import re
3032
import sys
3133
import sysconfig
@@ -116,6 +118,18 @@
116118
help="Print a list of module names to stdout and exit",
117119
)
118120

121+
parser.add_argument(
122+
"--generate-missing-stdlib-info",
123+
action="store_true",
124+
help="Generate file with stdlib module info",
125+
)
126+
127+
parser.add_argument(
128+
"--with-missing-stdlib-config",
129+
metavar="CONFIG_FILE",
130+
help="Path to JSON config file with custom missing module messages",
131+
)
132+
119133

120134
@enum.unique
121135
class ModuleState(enum.Enum):
@@ -281,6 +295,39 @@ def list_module_names(self, *, all: bool = False) -> set[str]:
281295
names.update(WINDOWS_MODULES)
282296
return names
283297

298+
def generate_missing_stdlib_info(self, config_path: str | None = None) -> None:
299+
config_messages = {}
300+
if config_path:
301+
try:
302+
with open(config_path, encoding='utf-8') as f:
303+
config_messages = json.load(f)
304+
except (FileNotFoundError, json.JSONDecodeError) as e:
305+
raise RuntimeError(f"Failed to load missing stdlib config {config_path!r}") from e
306+
307+
messages = {}
308+
for name in WINDOWS_MODULES:
309+
messages[name] = f"Unsupported platform for Windows-only standard library module {name!r}"
310+
311+
for modinfo in self.modules:
312+
if modinfo.state in (ModuleState.DISABLED, ModuleState.DISABLED_SETUP):
313+
messages[modinfo.name] = f"Standard library module disabled during build {modinfo.name!r} was not found"
314+
elif modinfo.state == ModuleState.NA:
315+
messages[modinfo.name] = f"Unsupported platform for standard library module {modinfo.name!r}"
316+
317+
messages.update(config_messages)
318+
319+
content = f'''\
320+
# Standard library information used by the traceback module for more informative
321+
# ModuleNotFound error messages.
322+
# Generated by check_extension_modules.py
323+
324+
_MISSING_STDLIB_MODULE_MESSAGES = {pprint.pformat(messages)}
325+
'''
326+
327+
output_path = self.builddir / "_missing_stdlib_info.py"
328+
with open(output_path, "w", encoding="utf-8") as f:
329+
f.write(content)
330+
284331
def get_builddir(self) -> pathlib.Path:
285332
try:
286333
with open(self.pybuilddir_txt, encoding="utf-8") as f:
@@ -499,6 +546,9 @@ def main() -> None:
499546
names = checker.list_module_names(all=True)
500547
for name in sorted(names):
501548
print(name)
549+
elif args.generate_missing_stdlib_info:
550+
checker.check()
551+
checker.generate_missing_stdlib_info(args.with_missing_stdlib_config)
502552
else:
503553
checker.check()
504554
checker.summary(verbose=args.verbose)

configure

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,15 @@ if test "$with_pkg_config" = yes -a -z "$PKG_CONFIG"; then
307307
AC_MSG_ERROR([pkg-config is required])]
308308
fi
309309

310+
dnl Allow distributors to provide custom missing stdlib module error messages
311+
AC_ARG_WITH([missing-stdlib-config],
312+
[AS_HELP_STRING([--with-missing-stdlib-config=FILE],
313+
[File with custom module error messages for missing stdlib modules])],
314+
[MISSING_STDLIB_CONFIG="$withval"],
315+
[MISSING_STDLIB_CONFIG=""]
316+
)
317+
AC_SUBST([MISSING_STDLIB_CONFIG])
318+
310319
# Set name for machine-dependent library files
311320
AC_ARG_VAR([MACHDEP], [name for machine-dependent library files])
312321
AC_MSG_CHECKING([MACHDEP])

0 commit comments

Comments
 (0)