Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections.abc
import itertools
import linecache
import os
import sys
import textwrap
import types
Expand All @@ -12,6 +13,7 @@
import tokenize
import io
import importlib.util
import pathlib
import _colorize

from contextlib import suppress
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :exc:`ModuleNotFoundError` hints when a module for a different ABI
exists.
Loading