mirror of
https://github.com/python/cpython.git
synced 2026-04-14 07:41:00 +00:00
GH-145006: add ModuleNotFoundError hints when a module for a differen… (#145007)
* GH-145006: add ModuleNotFoundError hints when a module for a different ABI exists Signed-off-by: Filipe Laíns <lains@riseup.net> * Fix deprecation warnings Signed-off-by: Filipe Laíns <lains@riseup.net> * Use SHLIB_SUFFIX in test_find_incompatible_extension_modules when available Signed-off-by: Filipe Laíns <lains@riseup.net> * Add test_incompatible_extension_modules_hint Signed-off-by: Filipe Laíns <lains@riseup.net> * Fix Windows Signed-off-by: Filipe Laíns <lains@riseup.net> * Show the whole extension module file name in hint Signed-off-by: Filipe Laíns <lains@riseup.net> --------- Signed-off-by: Filipe Laíns <lains@riseup.net>
This commit is contained in:
parent
4e45c9c309
commit
1ac9d138ae
3 changed files with 92 additions and 1 deletions
|
|
@ -9,15 +9,18 @@
|
|||
import builtins
|
||||
import unittest
|
||||
import unittest.mock
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
import random
|
||||
import string
|
||||
import importlib.machinery
|
||||
import sysconfig
|
||||
from test import support
|
||||
import shutil
|
||||
from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
|
||||
requires_debug_ranges, has_no_debug_ranges,
|
||||
requires_subprocess)
|
||||
requires_subprocess, os_helper)
|
||||
from test.support.os_helper import TESTFN, temp_dir, unlink
|
||||
from test.support.script_helper import assert_python_ok, assert_python_failure, make_script
|
||||
from test.support.import_helper import forget
|
||||
|
|
@ -5194,6 +5197,56 @@ def test_windows_only_module_error(self):
|
|||
else:
|
||||
self.fail("ModuleNotFoundError was not raised")
|
||||
|
||||
@unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules')
|
||||
def test_find_incompatible_extension_modules(self):
|
||||
"""_find_incompatible_extension_modules assumes the last extension in
|
||||
importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c)
|
||||
is untagged (eg. .so, .pyd).
|
||||
|
||||
This test exists to make sure that assumption is correct.
|
||||
"""
|
||||
last_extension_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
|
||||
if shlib_suffix := sysconfig.get_config_var('SHLIB_SUFFIX'):
|
||||
self.assertEqual(last_extension_suffix, shlib_suffix)
|
||||
else:
|
||||
before_dot, *extensions = last_extension_suffix.split('.')
|
||||
expected_prefixes = ['']
|
||||
if os.name == 'nt':
|
||||
# Windows puts the debug tag in the module file stem (eg. foo_d.pyd)
|
||||
expected_prefixes.append('_d')
|
||||
self.assertIn(before_dot, expected_prefixes, msg=(
|
||||
f'Unexpected prefix {before_dot!r} in extension module '
|
||||
f'suffix {last_extension_suffix!r}. '
|
||||
'traceback._find_incompatible_extension_module needs to be '
|
||||
'updated to take this into account!'
|
||||
))
|
||||
# if SHLIB_SUFFIX is not define, we assume the native
|
||||
# shared library suffix only contains one extension
|
||||
# (eg. '.so', bad eg. '.cpython-315-x86_64-linux-gnu.so')
|
||||
self.assertEqual(len(extensions), 1, msg=(
|
||||
'The last suffix in importlib.machinery.EXTENSION_SUFFIXES '
|
||||
'contains more than one extension, so it is probably different '
|
||||
'than SHLIB_SUFFIX. It probably contains an ABI tag! '
|
||||
'If this is a false positive, define SHLIB_SUFFIX in sysconfig.'
|
||||
))
|
||||
|
||||
@unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules')
|
||||
def test_incompatible_extension_modules_hint(self):
|
||||
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
|
||||
with os_helper.temp_dir() as tmp:
|
||||
# create a module with a incompatible ABI tag
|
||||
incompatible_module = f'foo.some-abi{untagged_suffix}'
|
||||
open(os.path.join(tmp, incompatible_module), "wb").close()
|
||||
# try importing it
|
||||
code = f'''
|
||||
import sys
|
||||
sys.path.insert(0, {tmp!r})
|
||||
import foo
|
||||
'''
|
||||
_, _, stderr = assert_python_failure('-c', code, __cwd=tmp)
|
||||
hint = f'Although a module with this name was found for a different Python version ({incompatible_module}).'
|
||||
self.assertIn(hint, stderr.decode())
|
||||
|
||||
|
||||
class TestColorizedTraceback(unittest.TestCase):
|
||||
maxDiff = None
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import collections.abc
|
||||
import itertools
|
||||
import linecache
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
import tokenize
|
||||
import io
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import _colorize
|
||||
|
||||
from contextlib import suppress
|
||||
|
|
@ -1129,6 +1131,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
|
|||
self._str += (". Site initialization is disabled, did you forget to "
|
||||
+ "add the site-packages directory to sys.path "
|
||||
+ "or to enable your virtual environment?")
|
||||
elif abi_tag := _find_incompatible_extension_module(module_name):
|
||||
self._str += (
|
||||
". Although a module with this name was found for a "
|
||||
f"different Python version ({abi_tag})."
|
||||
)
|
||||
else:
|
||||
suggestion = _compute_suggestion_error(exc_value, exc_traceback, module_name)
|
||||
if suggestion:
|
||||
|
|
@ -1880,3 +1887,32 @@ def _levenshtein_distance(a, b, max_cost):
|
|||
# Everything in this row is too big, so bail early.
|
||||
return max_cost + 1
|
||||
return result
|
||||
|
||||
|
||||
def _find_incompatible_extension_module(module_name):
|
||||
import importlib.machinery
|
||||
import importlib.resources.readers
|
||||
|
||||
if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
|
||||
return
|
||||
|
||||
# We assume the last extension is untagged (eg. .so, .pyd)!
|
||||
# tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
|
||||
# tests that assumption.
|
||||
untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
|
||||
# On Windows the debug tag is part of the module file stem, instead of the
|
||||
# extension (eg. foo_d.pyd), so let's remove it and just look for .pyd.
|
||||
if os.name == 'nt':
|
||||
untagged_suffix = untagged_suffix.removeprefix('_d')
|
||||
|
||||
parent, _, child = module_name.rpartition('.')
|
||||
if parent:
|
||||
traversable = importlib.resources.files(parent)
|
||||
else:
|
||||
traversable = importlib.resources.readers.MultiplexedPath(
|
||||
*map(pathlib.Path, filter(os.path.isdir, sys.path))
|
||||
)
|
||||
|
||||
for entry in traversable.iterdir():
|
||||
if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix):
|
||||
return entry.name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Add :exc:`ModuleNotFoundError` hints when a module for a different ABI
|
||||
exists.
|
||||
Loading…
Add table
Add a link
Reference in a new issue