Skip to content

Commit 0219ab7

Browse files
committed
GH-145006: add ModuleNotFoundError hints when a module for a different ABI exists
Signed-off-by: Filipe Laíns <lains@riseup.net>
1 parent 930b3fd commit 0219ab7

File tree

3 files changed

+44
-0
lines changed

3 files changed

+44
-0
lines changed

Lib/test/test_traceback.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import tempfile
1414
import random
1515
import string
16+
import importlib.machinery
1617
from test import support
1718
import shutil
1819
from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
@@ -5194,6 +5195,16 @@ def test_windows_only_module_error(self):
51945195
else:
51955196
self.fail("ModuleNotFoundError was not raised")
51965197

5198+
def test_find_incompatible_extension_modules(self):
5199+
"""_find_incompatible_extension_modules assumes the last extension in
5200+
importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c)
5201+
is untagged (eg. .so, .pyd).
5202+
5203+
This test exists to make sure that assumption is correct.
5204+
"""
5205+
if importlib.machinery.EXTENSION_SUFFIXES:
5206+
self.assertEqual(len(importlib.machinery.EXTENSION_SUFFIXES[-1].split('.')), 2)
5207+
51975208

51985209
class TestColorizedTraceback(unittest.TestCase):
51995210
maxDiff = None

Lib/traceback.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import collections.abc
44
import itertools
55
import linecache
6+
import os
67
import sys
78
import textwrap
89
import types
@@ -1129,6 +1130,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11291130
self._str += (". Site initialization is disabled, did you forget to "
11301131
+ "add the site-packages directory to sys.path "
11311132
+ "or to enable your virtual environment?")
1133+
elif abi_tag := _find_incompatible_extension_module(module_name):
1134+
self._str += (
1135+
". Although a module with this name was found for a "
1136+
f"different Python version ({abi_tag})."
1137+
)
11321138
else:
11331139
suggestion = _compute_suggestion_error(exc_value, exc_traceback, module_name)
11341140
if suggestion:
@@ -1880,3 +1886,28 @@ def _levenshtein_distance(a, b, max_cost):
18801886
# Everything in this row is too big, so bail early.
18811887
return max_cost + 1
18821888
return result
1889+
1890+
1891+
def _find_incompatible_extension_module(module_name):
1892+
import importlib.machinery
1893+
import importlib.resources.readers
1894+
1895+
if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
1896+
return
1897+
1898+
# We assume the last extension is untagged (eg. .so, .pyd)!
1899+
# tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
1900+
# tests that assumption.
1901+
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
1902+
1903+
parent, _, child = module_name.rpartition('.')
1904+
if parent:
1905+
traversable = importlib.resources.files(parent)
1906+
else:
1907+
traversable = importlib.resources.readers.MultiplexedPath(
1908+
*filter(os.path.isdir, sys.path)
1909+
)
1910+
1911+
for entry in traversable.iterdir():
1912+
if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix):
1913+
return entry.name.removeprefix(child + '.').removesuffix(untagged_suffix)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :exc:`ModuleNotFoundError` hints when a module for a different ABI
2+
exists.

0 commit comments

Comments
 (0)