| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 4e45c9c commit 1ac9d13
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,15 +9,18 @@ | |||
| 9 | 9 | import builtins | |
| 10 | 10 | import unittest | |
| 11 | 11 | import unittest.mock | |
| 12 | + import os | ||
| 12 | 13 | import re | |
| 13 | 14 | import tempfile | |
| 14 | 15 | import random | |
| 15 | 16 | import string | |
| 17 | + import importlib.machinery | ||
| 18 | + import sysconfig | ||
| 16 | 19 | from test import support | |
| 17 | 20 | import shutil | |
| 18 | 21 | from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ, | |
| 19 | 22 | requires_debug_ranges, has_no_debug_ranges, | |
| 20 | - requires_subprocess) | ||
| 23 | + requires_subprocess, os_helper) | ||
| 21 | 24 | from test.support.os_helper import TESTFN, temp_dir, unlink | |
| 22 | 25 | from test.support.script_helper import assert_python_ok, assert_python_failure, make_script | |
| 23 | 26 | from test.support.import_helper import forget | |
@@ -5194,6 +5197,56 @@ def test_windows_only_module_error(self): | |||
| 5194 | 5197 | else: | |
| 5195 | 5198 | self.fail("ModuleNotFoundError was not raised") | |
| 5196 | 5199 | ||
| 5200 | + @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules') | ||
| 5201 | + def test_find_incompatible_extension_modules(self): | ||
| 5202 | + """_find_incompatible_extension_modules assumes the last extension in | ||
| 5203 | + importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c) | ||
| 5204 | + is untagged (eg. .so, .pyd). | ||
| 5205 | + | ||
| 5206 | + This test exists to make sure that assumption is correct. | ||
| 5207 | + """ | ||
| 5208 | + last_extension_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1] | ||
| 5209 | + if shlib_suffix := sysconfig.get_config_var('SHLIB_SUFFIX'): | ||
| 5210 | + self.assertEqual(last_extension_suffix, shlib_suffix) | ||
| 5211 | + else: | ||
| 5212 | + before_dot, *extensions = last_extension_suffix.split('.') | ||
| 5213 | + expected_prefixes = [''] | ||
| 5214 | + if os.name == 'nt': | ||
| 5215 | + # Windows puts the debug tag in the module file stem (eg. foo_d.pyd) | ||
| 5216 | + expected_prefixes.append('_d') | ||
| 5217 | + self.assertIn(before_dot, expected_prefixes, msg=( | ||
| 5218 | + f'Unexpected prefix {before_dot!r} in extension module ' | ||
| 5219 | + f'suffix {last_extension_suffix!r}. ' | ||
| 5220 | + 'traceback._find_incompatible_extension_module needs to be ' | ||
| 5221 | + 'updated to take this into account!' | ||
| 5222 | + )) | ||
| 5223 | + # if SHLIB_SUFFIX is not define, we assume the native | ||
| 5224 | + # shared library suffix only contains one extension | ||
| 5225 | + # (eg. '.so', bad eg. '.cpython-315-x86_64-linux-gnu.so') | ||
| 5226 | + self.assertEqual(len(extensions), 1, msg=( | ||
| 5227 | + 'The last suffix in importlib.machinery.EXTENSION_SUFFIXES ' | ||
| 5228 | + 'contains more than one extension, so it is probably different ' | ||
| 5229 | + 'than SHLIB_SUFFIX. It probably contains an ABI tag! ' | ||
| 5230 | + 'If this is a false positive, define SHLIB_SUFFIX in sysconfig.' | ||
| 5231 | + )) | ||
| 5232 | + | ||
| 5233 | + @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform does not support extension modules') | ||
| 5234 | + def test_incompatible_extension_modules_hint(self): | ||
| 5235 | + untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1] | ||
| 5236 | + with os_helper.temp_dir() as tmp: | ||
| 5237 | + # create a module with a incompatible ABI tag | ||
| 5238 | + incompatible_module = f'foo.some-abi{untagged_suffix}' | ||
| 5239 | + open(os.path.join(tmp, incompatible_module), "wb").close() | ||
| 5240 | + # try importing it | ||
| 5241 | + code = f''' | ||
| 5242 | + import sys | ||
| 5243 | + sys.path.insert(0, {tmp!r}) | ||
| 5244 | + import foo | ||
| 5245 | + ''' | ||
| 5246 | + _, _, stderr = assert_python_failure('-c', code, __cwd=tmp) | ||
| 5247 | + hint = f'Although a module with this name was found for a different Python version ({incompatible_module}).' | ||
| 5248 | + self.assertIn(hint, stderr.decode()) | ||
| 5249 | + | ||
| 5197 | 5250 | ||
| 5198 | 5251 | class TestColorizedTraceback(unittest.TestCase): | |
| 5199 | 5252 | maxDiff = None | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,6 +3,7 @@ | |||
| 3 | 3 | import collections.abc | |
| 4 | 4 | import itertools | |
| 5 | 5 | import linecache | |
| 6 | + import os | ||
| 6 | 7 | import sys | |
| 7 | 8 | import textwrap | |
| 8 | 9 | import types | |
@@ -12,6 +13,7 @@ | |||
| 12 | 13 | import tokenize | |
| 13 | 14 | import io | |
| 14 | 15 | import importlib.util | |
| 16 | + import pathlib | ||
| 15 | 17 | import _colorize | |
| 16 | 18 | ||
| 17 | 19 | from contextlib import suppress | |
@@ -1129,6 +1131,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, | |||
| 1129 | 1131 | self._str += (". Site initialization is disabled, did you forget to " | |
| 1130 | 1132 | + "add the site-packages directory to sys.path " | |
| 1131 | 1133 | + "or to enable your virtual environment?") | |
| 1134 | + elif abi_tag := _find_incompatible_extension_module(module_name): | ||
| 1135 | + self._str += ( | ||
| 1136 | + ". Although a module with this name was found for a " | ||
| 1137 | + f"different Python version ({abi_tag})." | ||
| 1138 | + ) | ||
| 1132 | 1139 | else: | |
| 1133 | 1140 | suggestion = _compute_suggestion_error(exc_value, exc_traceback, module_name) | |
| 1134 | 1141 | if suggestion: | |
@@ -1880,3 +1887,32 @@ def _levenshtein_distance(a, b, max_cost): | |||
| 1880 | 1887 | # Everything in this row is too big, so bail early. | |
| 1881 | 1888 | return max_cost + 1 | |
| 1882 | 1889 | return result | |
| 1890 | + | ||
| 1891 | + | ||
| 1892 | + def _find_incompatible_extension_module(module_name): | ||
| 1893 | + import importlib.machinery | ||
| 1894 | + import importlib.resources.readers | ||
| 1895 | + | ||
| 1896 | + if not module_name or not importlib.machinery.EXTENSION_SUFFIXES: | ||
| 1897 | + return | ||
| 1898 | + | ||
| 1899 | + # We assume the last extension is untagged (eg. .so, .pyd)! | ||
| 1900 | + # tests.test_traceback.MiscTest.test_find_incompatible_extension_modules | ||
| 1901 | + # tests that assumption. | ||
| 1902 | + untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1] | ||
| 1903 | + # On Windows the debug tag is part of the module file stem, instead of the | ||
| 1904 | + # extension (eg. foo_d.pyd), so let's remove it and just look for .pyd. | ||
| 1905 | + if os.name == 'nt': | ||
| 1906 | + untagged_suffix = untagged_suffix.removeprefix('_d') | ||
| 1907 | + | ||
| 1908 | + parent, _, child = module_name.rpartition('.') | ||
| 1909 | + if parent: | ||
| 1910 | + traversable = importlib.resources.files(parent) | ||
| 1911 | + else: | ||
| 1912 | + traversable = importlib.resources.readers.MultiplexedPath( | ||
| 1913 | + *map(pathlib.Path, filter(os.path.isdir, sys.path)) | ||
| 1914 | + ) | ||
| 1915 | + | ||
| 1916 | + for entry in traversable.iterdir(): | ||
| 1917 | + if entry.name.startswith(child + '.') and entry.name.endswith(untagged_suffix): | ||
| 1918 | + return entry.name | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,2 @@ | |||
| 1 | + Add :exc:`ModuleNotFoundError` hints when a module for a different ABI | ||
| 2 | + exists. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments