Skip to content

Commit b33d941

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 b33d941

File tree

3 files changed

+36
-0
lines changed

3 files changed

+36
-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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
11291129
self._str += (". Site initialization is disabled, did you forget to "
11301130
+ "add the site-packages directory to sys.path "
11311131
+ "or to enable your virtual environment?")
1132+
elif abi_tag := _find_incompatible_extension_module(module_name):
1133+
self._str += (
1134+
". Although a module with this name was found for a "
1135+
f"different Python version ({abi_tag})."
1136+
)
11321137
else:
11331138
suggestion = _compute_suggestion_error(exc_value, exc_traceback, module_name)
11341139
if suggestion:
@@ -1880,3 +1885,21 @@ def _levenshtein_distance(a, b, max_cost):
18801885
# Everything in this row is too big, so bail early.
18811886
return max_cost + 1
18821887
return result
1888+
1889+
1890+
def _find_incompatible_extension_module(module_name):
1891+
import importlib.machinery
1892+
import importlib.resources
1893+
1894+
if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
1895+
return
1896+
1897+
# We assume the last extension is untagged (eg. .so, .pyd)!
1898+
# tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
1899+
# tests that assumption.
1900+
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
1901+
1902+
parent, _, child = module_name.rpartition('.')
1903+
for entry in importlib.resources.files(parent).iterdir():
1904+
if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix):
1905+
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)