A way to get all of the modules

Now it only supported for the import from normal way(only from sys.path). The custom way to import is uncertain.

Welcome to share your idea if you have. If there is a way to handle with it, I will feel good.

There isn’t a straightforward way to comprehensively handle arbitrary import hooks in the general case, which is why you’re not hearing a lot of enthusiasm from core devs for the idea.

“In the face of ambiguity, refuse the temptation to guess” definitely applies when it comes to comparing attempted imports to a full installation environment where all sorts of import system customisation might be going on (vs the more limited activity of comparing attempted imports to the relatively small and well known set of standard library module names).

It’s a fine idea as a third party project though, as folks can consider whether its suitable for their personal development environments before decided whether they want to make it part of their development workflow.

(I personally wouldn’t use it, as for me it fits in a niche that is already covered by VS Code’s static code analysis, but I can see the potential utility in some other use cases)

So how to define the way in this project? It only scans all of the path in sys.path to get the reachable module. The other ways are ignore. But it is not only compare with the stdlib (although it first).

That’s up to you, based on your goals for the project. The point I’m making is that there’s no way of doing this that I’d consider acceptable as a core feature. Third party projects have a lot more room for flexibility here (if you don’t support import hooks, people who care can simply ignore your project).

1 Like

If I say that I have an ambition that finally make the project as a new python feature, what can I do?

Come up with good answers to people’s questions. Unfortunately, you’re going to have to work on it, particularly if it’s important to you.

Work on your PyPI library and aim to have it handle package discovery in increasingly more complex environments.

What you’re proposing isn’t fundamentally impossible, since static code analysers do this kind of thing when they complain they can’t find an import target in the code they’re analysing and offer suggestions for other lexically close names that are importable.

Instead, the concern is that solving the general case is sufficiently complex that it isn’t something we want to maintain as part of the core import system (since that’s already spectacularly complicated, we’re not exactly keen to incur significant additional complexity for the sake of improving error messages in a case that external development tools can already help with).

Static analysers also have the # noqa escape hatch available for situations where the code is doing something that the import system can handle but the static analyser can’t. A runtime error doesn’t have that additional level of configurability, so in trying to be more helpful, it could end up steering users in the wrong direction entirely - a bad hint can easily end up being worse than offering no hint at all.

In implementing the proposal as a third party package, there are a few potential outcomes:

  • as you explore the general case, you come to agree that the complexity needed is too high to justify adding the feature as a standard component
  • solving the general case proves simpler than we expect and you’re able to make a successful proposal at a later date
  • the disagreement over “how complex is too complex?” remains unresolved indefinitely, but the opt-in library based solution is available to those that want to use it
4 Likes

Several days ago, I make an idea in the third party modules that improve and suggest name in ModuleNotFoundError(import xxx.yyy.zzz no longer just “No module named ‘xxx.yyy.zzz’”, instead, use different message in different conditions such as “No module named ‘xxx’” or “module ‘xxx’ has no child module ‘yyy’”, and give possible suggestion if it has).

Now with the module “friendly_module_not_found_error” version 0.1.3 published, it also supports for custom import hook, but it needs a magic method “__find__”. It can return all of the top module that the hook can import if no argument given and all of the submodule of the module named given argument “name”.

Now I need for more feedback for it. Now the mainly changed in traceback here:

def _compute_suggestion_error(exc_value, tb, wrong_name):
    if wrong_name is None or not isinstance(wrong_name, str):
        return None
    if isinstance(exc_value, AttributeError):
        obj = exc_value.obj
        try:
            try:
                d = dir(obj)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            hide_underscored = (wrong_name[:1] != '_')
            if hide_underscored and tb is not None:
                while tb.tb_next is not None:
                    tb = tb.tb_next
                frame = tb.tb_frame
                if 'self' in frame.f_locals and frame.f_locals['self'] is obj:
                    hide_underscored = False
            if hide_underscored:
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return None
    elif isinstance(exc_value, ImportError):
        try:
            mod = __import__(exc_value.name)
            try:
                d = dir(mod)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(mod.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            if wrong_name[:1] != '_':
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return _suggest_for_module(exc_value)   # - return None                     
    else:
        assert isinstance(exc_value, NameError)
        # find most recent frame
        if tb is None:
            return None
        while tb.tb_next is not None:
            tb = tb.tb_next
        frame = tb.tb_frame
        d = (
            list(frame.f_locals)
            + list(frame.f_globals)
            + list(frame.f_builtins)
        )
        d = [x for x in d if isinstance(x, str)]

        # Check first if we are in a method and the instance
        # has the wrong name as attribute
        if 'self' in frame.f_locals:
            self = frame.f_locals['self']
            try:
                has_wrong_name = hasattr(self, wrong_name)
            except Exception:
                has_wrong_name = False
            if has_wrong_name:
                return f"self.{wrong_name}"

    return _calculate_closed_name(wrong_name, d)  # change to the standalone function

def _calculate_closed_name(wrong_name, d):
    try:
        d.sort()
    except:
        pass
    try:
        import _suggestions
        return _suggestions._generate_suggestions(d, wrong_name)
    except ImportError:
        if len(d) > _MAX_CANDIDATE_ITEMS:
            return None
        wrong_name_len = len(wrong_name)
        if wrong_name_len > _MAX_STRING_SIZE:
            return None
        best_distance = wrong_name_len
        suggestion = None
        d.sort()
        for possible_name in d:
            if possible_name == wrong_name:
                # A missing attribute is "found". Don't suggest it (see GH-88821).
                continue
            # No more than 1/3 of the involved characters should need changed.
            max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
            # Don't take matches we've already beaten.
            max_distance = min(max_distance, best_distance - 1)
            current_distance = _levenshtein_distance(wrong_name, possible_name, max_distance)
            if current_distance > max_distance:
                continue
            if not suggestion or current_distance < best_distance:
                suggestion = possible_name
                best_distance = current_distance
        return suggestion

def _suggest_for_module(exc_value):
    import sys
    import os
    from importlib import machinery
    
    def scan_dir(path):
        """
        Return all of the packages in the path without import
        contains
          - .py file
          - directory with "__init__.py"
          - the .pyd/so file that has right ABI
        """
        if not os.path.isdir(path):
            return []
    
        suffixes = machinery.EXTENSION_SUFFIXES
        result = []
    
        for name in os.listdir(path):
            full_path = os.path.join(path, name)
    
            # .py file
            if name.endswith(".py") and os.path.isfile(full_path):
                modname = name[:-3]
                if modname.isidentifier():
                    result.append(modname)
    
            # directory with "__init__.py"
            elif os.path.isdir(full_path):
                init_file = os.path.join(full_path, "__init__.py")
                if os.path.isfile(init_file) and name.isidentifier():
                    result.append(name)
    
            # the .pyd/so file that has right ABI
            elif os.path.isfile(full_path):
                for suf in suffixes:
                    if name.endswith(suf):
                        modname = name[:-len(suf)]
                        if modname.isidentifier():
                            result.append(modname)
                        break
    
        return sorted(result)
    
    def find_all_packages():
        list_d = []
        for hook in sys.meta_path:
            try:
                func = getattr(hook, "__find__", None)
                if callable(func):
                    list_d.append(func())
            except:
                list_d.append([])
        for i in sys.path:
            if isinstance(i, str) and not i.endswith("idlelib"):
                list_d.append(scan_dir(i))
        list_d.append(sorted(sys.builtin_module_names))
        return list_d

    def compare_top_module(module_name):
        result = _calculate_closed_name(module_name, sorted(sys.stdlib_module_names))
        if result:
            return result
        other_result_list = []
        for i in list_d:
            result = _calculate_closed_name(module_name, i)
            if result:
                return result

    def handle_hook_module(name, i):
        """
        ```
        def __find__(self, name: str=None) -> List[str]:
            return []
        ```
        `__find__` method should return a list about the modules without import them.
        when name is not None, it should return the submodule below it.
        """
        for i in sys.meta_path:
            try:
                func = getattr(i, "__find__", None)
                if callable(func):
                    list_d = sorted(func(name))
                    if i in list_d:
                        exc_value.msg += ", but it appear in the result from '__find__'. Is your code wrong?"
                        return i
                    result = _calculate_closed_name(i, list_d)
                    if result:
                        return result
            except:
                continue

    def handle_hook_wrong_module(hook, wrong_name_list):
        module_name = wrong_name_list[0]
        for i in wrong_name_list[1:]:
            exc_value.msg = f"module '{module_name}' has no child module '{i}'"
            try:
                func = getattr(hook, "__find__", None)                
                if callable(func):
                    list_d = sorted(func(module_name))
                    if i not in list_d:                        
                        return _calculate_closed_name(i, list_d)
            except:
                return
            module_name += "." + i
        exc_value.msg += ", but it appear in the final result from '__find__'. Is your code wrong?"
        
    if not isinstance(exc_value, ModuleNotFoundError):
        return
    list_d = find_all_packages()            
    _module_name = exc_value.name
    wrong_name_list = _module_name.split(".")
    module_name = wrong_name_list[0]
    if module_name in sys.modules:
        for i in wrong_name_list[1:]:
            original_module_name = module_name
            module_name += "." + i
            if module_name in sys.modules:
                continue            
            exc_value.msg = f"module '{original_module_name}' has no child module '{i}'"
            if hasattr(sys.modules[original_module_name], "__path__"):
                d=[]
                for ii in sys.modules[original_module_name].__path__:
                    d += scan_dir(ii)
                wrong_name = i
                return _calculate_closed_name(wrong_name, d)
            else:
                result = handle_hook_module(original_module_name, i)
                if result:
                    if result == i:
                        return
                    return result
                exc_value.msg += f"; '{original_module_name}' is not a package"
                return
    else:
        if len(wrong_name_list) == 1 or module_name not in sum(list_d, []):
            return compare_top_module(module_name)
        else:
            for i in sys.meta_path:
                try:
                    func = getattr(i, "__find__", None)
                    if callable(func):
                        if module_name in func():
                            return handle_hook_wrong_module(i, wrong_name_list)
                except:
                    continue
            for i in sys.path:
                if module_name in scan_dir(i):
                    module_path = f"{i}/{module_name}"
                    break
            else:
                return compare_top_module(module_name)
            for i in wrong_name_list[1:]:
                if not os.path.exists(module_path) or not os.path.isdir(module_path):
                    exc_value.msg = f"module '{module_name}' has no child module '{i}'; '{module_name}' is not a package"
                    return
                original_module_name = module_name
                module_name += "." + i
                d = scan_dir(module_path)
                if i not in scan_dir(module_path):
                    exc_value.msg = f"module '{original_module_name}' has no child module '{i}'"
                    return _calculate_closed_name(i, d)
                module_path += f"/{i}"
3 Likes

With update the project from time to time, now the suggest for ModuleNotFoundError has been mature. Now the mainly code below:

def _compute_suggestion_error(exc_value, tb, wrong_name):
    if wrong_name is None or not isinstance(wrong_name, str):
        return None
    if isinstance(exc_value, AttributeError):
        obj = exc_value.obj
        try:
            try:
                d = dir(obj)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            hide_underscored = (wrong_name[:1] != '_')
            if hide_underscored and tb is not None:
                while tb.tb_next is not None:
                    tb = tb.tb_next
                frame = tb.tb_frame
                if 'self' in frame.f_locals and frame.f_locals['self'] is obj:
                    hide_underscored = False
            if hide_underscored:
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return None
    elif isinstance(exc_value, ImportError):
        try:
            mod = __import__(exc_value.name)
            try:
                d = dir(mod)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(mod.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            if wrong_name[:1] != '_':
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return _suggest_for_module(exc_value)                        
    else:
        assert isinstance(exc_value, NameError)
        # find most recent frame
        if tb is None:
            return None
        while tb.tb_next is not None:
            tb = tb.tb_next
        frame = tb.tb_frame
        d = (
            list(frame.f_locals)
            + list(frame.f_globals)
            + list(frame.f_builtins)
        )
        d = [x for x in d if isinstance(x, str)]

        # Check first if we are in a method and the instance
        # has the wrong name as attribute
        if 'self' in frame.f_locals:
            self = frame.f_locals['self']
            try:
                has_wrong_name = hasattr(self, wrong_name)
            except Exception:
                has_wrong_name = False
            if has_wrong_name:
                return f"self.{wrong_name}"

    return _calculate_closed_name(wrong_name, d)

def _calculate_closed_name(wrong_name, d):
    try:
        d.sort()
    except:
        pass
    try:
        import _suggestions
        return _suggestions._generate_suggestions(d, wrong_name)
    except ImportError:
        if len(d) > _MAX_CANDIDATE_ITEMS:
            return None
        wrong_name_len = len(wrong_name)
        if wrong_name_len > _MAX_STRING_SIZE:
            return None
        best_distance = wrong_name_len
        suggestion = None
        d.sort()
        for possible_name in d:
            if possible_name == wrong_name:
                # A missing attribute is "found". Don't suggest it (see GH-88821).
                continue
            # No more than 1/3 of the involved characters should need changed.
            max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
            # Don't take matches we've already beaten.
            max_distance = min(max_distance, best_distance - 1)
            current_distance = _levenshtein_distance(wrong_name, possible_name, max_distance)
            if current_distance > max_distance:
                continue
            if not suggestion or current_distance < best_distance:
                suggestion = possible_name
                best_distance = current_distance
        return suggestion

def _suggest_for_module(exc_value):
    import sys
    import os
    from importlib import machinery
    
    def scan_dir(path):
        """
        Return all of the packages in the path without import
        contains
          - .py file
          - directory with "__init__.py"
          - the .pyd/so file that has right ABI
        """
        if not os.path.isdir(path):
            return []
    
        suffixes = machinery.EXTENSION_SUFFIXES
        result = []
    
        for name in os.listdir(path):
            full_path = os.path.join(path, name)
    
            # .py file
            if name.endswith(".py") and os.path.isfile(full_path):
                modname = name[:-3]
                if modname.isidentifier():
                    result.append(modname)
    
            # directory with "__init__.py"
            elif os.path.isdir(full_path):
                init_file = os.path.join(full_path, "__init__.py")
                if os.path.isfile(init_file) and name.isidentifier():
                    result.append(name)
    
            # the .pyd/so file that has right ABI
            elif os.path.isfile(full_path):
                for suf in suffixes:
                    if name.endswith(suf):
                        modname = name[:-len(suf)]
                        if modname.isidentifier():
                            result.append(modname)
                        break
    
        return sorted(result)
    
    def find_all_packages():
        return [scan_dir(i) if isinstance(i, str) and
                not i.endswith("idlelib") else []
                for i in sys.path] + [sorted(sys.builtin_module_names)]

    def compare_top_module(module_name):
        result = _calculate_closed_name(module_name, sorted(sys.stdlib_module_names))
        if result:
            return result
        other_result_list = []
        for i in list_d:
            result = _calculate_closed_name(module_name, i)
            if result:
                other_result_list.append(result)
        if other_result_list:
            return other_result_list[0]
        else:
            return

    def handle_wrong_module(module_name, path, child_module_list):
        for i in child_module_list:
            exc_value.msg = f"module '{module_name}' has no child module '{i}'"
            if not os.path.exists(path) or not os.path.isdir(path):
                exc_value.msg += f"; {module_name} is not a package"
                return
            list_d = scan_dir(path)
            if i not in list_d:
                return _calculate_closed_name(i, list_d)
            path = os.path.join(path, i)
            module_name += f".{i}"
        
    if not isinstance(exc_value, ModuleNotFoundError):
        return
    list_d = find_all_packages()            
    _module_name = exc_value.name
    wrong_name_list = _module_name.split(".")
    module_name = wrong_name_list[0]
    if module_name in sys.modules:
        wrong_name_copy = wrong_name_list[1:]
        for i in wrong_name_list[1:]:
            original_module_name = module_name
            module_name += "." + i
            wrong_name_copy.pop(0)
            if module_name in sys.modules:
                continue            
            exc_value.msg = f"module '{original_module_name}' has no child module '{i}'"
            if hasattr(sys.modules[original_module_name], "__path__"):
                d=[]
                for ii in sys.modules[original_module_name].__path__:
                    list_path = scan_dir(ii)
                    if i in list_path:
                        return handle_wrong_module(module_name, os.path.join(ii, i), wrong_name_copy)
                    d += list_path
                wrong_name = i
                return _calculate_closed_name(wrong_name, d)
            else:
                exc_value.msg += f"; '{original_module_name}' is not a package"
                return
    else:
        if len(wrong_name_list) == 1 or module_name not in sum(list_d, []):
            return compare_top_module(module_name)
        else:
            for i in sys.path:
                if module_name in scan_dir(i):
                    module_path = f"{i}/{module_name}"
                    break
            else:
                return compare_top_module(module_name)
            return handle_wrong_module(module_name, module_path, wrong_name_list[1:])

Is the version that able to PR to python?
This code will change the message of ModuleNotFoundError and make it more friendly: where the module name wrong and give the suggestion on it.
More detail here.

1 Like

Does it address the issues raised above with custom import hooks? If not, I’d say the answer is “no”.

Now the third-party version does, but it need a new method “__find__” that defined by users so I worry about that whether that is suitable for PR to python.

If it involves changes to the import machinery, I think it needs a PEP.

In fact. If it needs to support for custom import hooks, the PEP is needed. However, in my original plan, the supporting for custom import hooks is site-package-version only.

Here is the fake code in module “importlib.abc” if the suggestion should be given for that condition:

class MetaPathFinder(metaclass=abc.ABCMeta):

    """Abstract base class for import finders on sys.meta_path."""

    # We don't define find_spec() here since that would break
    # hasattr checks we do to support backward compatibility.

    def invalidate_caches(self):
        """An optional method for clearing the finder's cache, if any.
        This method is used by importlib.invalidate_caches().
        """

    def __find__(self, name: str=None) -> list[str]:
        """An agreement for suggest the module name when ModuleNotFoundError raise
        This method should return all of the submodules under the module given.
        If name is None, it return all of the top modules.
        This method shouldn't import any modules.
        """
        return []

_register(MetaPathFinder, machinery.BuiltinImporter, machinery.FrozenImporter,
          machinery.PathFinder, machinery.WindowsRegistryFinder)

The method “__find__” is the new method.

1 Like

If the name of the method shouldn’t be the dunder, the new name should be hardly found in various projects now, so that we can minimize the comflicts.

Now I think that if we can change the message and the attribute from “importlib”, the code might be better. However, the change will be too large because it changes the attribute name of “ModuleNotFoundError”.

Here is step 2 code (Now test in sitecustomize.py):

import importlib._bootstrap
import importlib._bootstrap_external
import _imp
import os
import sys
import traceback

def scan_dir(path):
    """
    Return all of the packages in the path without import
    contains
      - .py file
      - directory with "__init__.py"
      - the .pyd/so file that has right ABI
    """
    if not os.path.isdir(path):
        return []

    suffixes = _imp.extension_suffixes()
    result = []

    for name in os.listdir(path):
        full_path = os.path.join(path, name)

        # .py file
        if name.endswith(".py") and os.path.isfile(full_path):
            modname = name[:-3]
            if modname.isidentifier():
                result.append(modname)

        # directory with "__init__.py"
        elif os.path.isdir(full_path):
            init_file = os.path.join(full_path, "__init__.py")
            if os.path.isfile(init_file) and name.isidentifier():
                result.append(name)

        # the .pyd/so file that has right ABI
        elif os.path.isfile(full_path):
            for suf in suffixes:
                if name.endswith(suf):
                    modname = name[:-len(suf)]
                    if modname.isidentifier():
                        result.append(modname)
                    break

    return sorted(result)

_ERR_MSG_PREFIX = 'No module named '
_CHILD_ERR_MSG = 'module {!r} has no child module {!r}'
_ERR_MSG = _ERR_MSG_PREFIX + '{!r}'

def _find_and_load_unlocked(name, import_):
    path = None
    parent, _, child = name.rpartition('.')
    parent_spec = None
    if parent:
        if parent not in sys.modules:
            importlib._bootstrap._call_with_frames_removed(import_, parent)
        # Crazy side-effects!
        if name in sys.modules:
            return sys.modules[name]
        parent_module = sys.modules[parent]
        try:
            path = parent_module.__path__
        except AttributeError:
            msg = _CHILD_ERR_MSG.format(parent, child) + f'; {parent!r} is not a package'
            raise ModuleNotFoundError(msg, name=child) from None
        parent_spec = parent_module.__spec__
    spec = importlib._bootstrap._find_spec(name, path)
    if spec is None:
        if not parent:
            msg = f'{_ERR_MSG_PREFIX}{name!r}'
        else:
            msg = _CHILD_ERR_MSG.format(parent, child)
        error = ModuleNotFoundError(msg, name=child)
        suggest_list = []
        for i in sys.meta_path:
            func = getattr(i, '__find__', None)
            if callable(func):
                list_d = func(parent)
                if list_d:
                    suggest_list.append(list_d)
        if not parent:
            for paths in sys.path:
                suggest_list.append(scan_dir(paths))
        else:
            suggest_list.append(find_in_path(parent))
        error._suggestion = suggest_list
        raise error
    else:
        if parent_spec:
            # Temporarily add child we are currently importing to parent's
            # _uninitialized_submodules for circular import tracking.
            parent_spec._uninitialized_submodules.append(child)
        try:
            module = importlib._bootstrap._load_unlocked(spec)
        finally:
            if parent_spec:
                parent_spec._uninitialized_submodules.pop()
    if parent:
        # Set the module as an attribute on its parent.
        parent_module = sys.modules[parent]
        try:
            setattr(parent_module, child, module)
        except AttributeError:
            msg = f"Cannot set an attribute on {parent!r} for child module {child!r}"
            importlib._bootstrap._warnings.warn(msg, ImportWarning)
    return module

importlib._bootstrap._find_and_load_unlocked = _find_and_load_unlocked
importlib._bootstrap.BuiltinImporter.__find__ = staticmethod(lambda name=None: (sorted(sys.builtin_module_names) if not name else []))

def find_in_path(name=None):
    if not name:
        return []
    name_list = name.split(".")
    for i in sys.path:
        list_d = scan_dir(i)
        if name_list[0] in list_d:
            break
    else:
        return []
    path = i
    for j in name_list:
        path = os.path.join(path, j)
    if not os.path.isdir(path):
        return []
    if not os.path.exists(os.path.join(path, "__init__.py")):
        return []
    return scan_dir(path)

def _compute_suggestion_error(exc_value, tb, wrong_name):
    if wrong_name is None or not isinstance(wrong_name, str):
        return None
    if isinstance(exc_value, AttributeError):
        obj = exc_value.obj
        try:
            try:
                d = dir(obj)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(obj.__class__.__dict__.keys()) + list(obj.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            hide_underscored = (wrong_name[:1] != '_')
            if hide_underscored and tb is not None:
                while tb.tb_next is not None:
                    tb = tb.tb_next
                frame = tb.tb_frame
                if 'self' in frame.f_locals and frame.f_locals['self'] is obj:
                    hide_underscored = False
            if hide_underscored:
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return _handle_module(exc_value)
    elif isinstance(exc_value, ImportError):
        if isinstance(exc_value, ModuleNotFoundError):
            return _handle_module(exc_value)
        try:
            mod = __import__(exc_value.name)
            try:
                d = dir(mod)
            except TypeError:  # Attributes are unsortable, e.g. int and str
                d = list(mod.__dict__.keys())
            d = sorted([x for x in d if isinstance(x, str)])
            if wrong_name[:1] != '_':
                d = [x for x in d if x[:1] != '_']
        except Exception:
            return None
    else:
        assert isinstance(exc_value, NameError)
        # find most recent frame
        if tb is None:
            return None
        while tb.tb_next is not None:
            tb = tb.tb_next
        frame = tb.tb_frame
        d = (
            list(frame.f_locals)
            + list(frame.f_globals)
            + list(frame.f_builtins)
        )
        d = [x for x in d if isinstance(x, str)]

        # Check first if we are in a method and the instance
        # has the wrong name as attribute
        if 'self' in frame.f_locals:
            self = frame.f_locals['self']
            try:
                has_wrong_name = hasattr(self, wrong_name)
            except Exception:
                has_wrong_name = False
            if has_wrong_name:
                return f"self.{wrong_name}"

    return _calculate_closed_name(wrong_name, d)

def _handle_module(exc_value):
    if not isinstance(exc_value, ModuleNotFoundError):
        return
    if len(exc_value.name) > traceback._MAX_STRING_SIZE:
        return
    all_result = []
    for i in getattr(exc_value, "_suggestion", []):
        if exc_value.name in i:
            return exc_value.name
        result = _calculate_closed_name(exc_value.name, i)
        if result:
            all_result.append(result)
    return _calculate_closed_name(exc_value.name, sorted(all_result))

def _calculate_closed_name(wrong_name, d):
    try:
        import _suggestions
    except ImportError:
        pass
    else:
        return _suggestions._generate_suggestions(d, wrong_name)

    # Compute closest match

    if len(d) > traceback._MAX_CANDIDATE_ITEMS:
        return None
    wrong_name_len = len(wrong_name)
    if wrong_name_len > traceback._MAX_STRING_SIZE:
        return None
    best_distance = wrong_name_len
    suggestion = None
    for possible_name in d:
        if possible_name == wrong_name:
            # A missing attribute is "found". Don't suggest it (see GH-88821).
            continue
        # No more than 1/3 of the involved characters should need changed.
        max_distance = (len(possible_name) + wrong_name_len + 3) * _MOVE_COST // 6
        # Don't take matches we've already beaten.
        max_distance = min(max_distance, best_distance - 1)
        current_distance = traceback._levenshtein_distance(wrong_name, possible_name, max_distance)
        if current_distance > max_distance:
            continue
        if not suggestion or current_distance < best_distance:
            suggestion = possible_name
            best_distance = current_distance
    return suggestion

traceback._compute_suggestion_error = _compute_suggestion_error

def new_init(self, exc_type, exc_value, exc_traceback, *, limit=None,
            lookup_lines=True, capture_locals=False, compact=False,
            max_group_width=15, max_group_depth=10, save_exc_type=True, _seen=None):
    # NB: we need to accept exc_traceback, exc_value, exc_traceback to
    # permit backwards compat with the existing API, otherwise we
    # need stub thunk objects just to glue it together.
    # Handle loops in __cause__ or __context__.
    is_recursive_call = _seen is not None
    if _seen is None:
        _seen = set()
    _seen.add(id(exc_value))

    self.max_group_width = max_group_width
    self.max_group_depth = max_group_depth

    self.stack = traceback.StackSummary._extract_from_extended_frame_gen(
            traceback._walk_tb_with_full_positions(exc_traceback),
            limit=limit, lookup_lines=lookup_lines,
            capture_locals=capture_locals)

    self._exc_type = exc_type if save_exc_type else None

    # Capture now to permit freeing resources: only complication is in the
    # unofficial API _format_final_exc_line
    self._str = traceback._safe_string(exc_value, 'exception')
    try:
        self.__notes__ = getattr(exc_value, '__notes__', None)
    except Exception as e:
        self.__notes__ = [
            f'Ignored error getting __notes__: {_safe_string(e, '__notes__', repr)}']

    self._is_syntax_error = False
    self._have_exc_type = exc_type is not None
    if exc_type is not None:
        self.exc_type_qualname = exc_type.__qualname__
        self.exc_type_module = exc_type.__module__
    else:
        self.exc_type_qualname = None
        self.exc_type_module = None

    if exc_type and issubclass(exc_type, SyntaxError):
        # Handle SyntaxError's specially
        self.filename = exc_value.filename
        lno = exc_value.lineno
        self.lineno = str(lno) if lno is not None else None
        end_lno = exc_value.end_lineno
        self.end_lineno = str(end_lno) if end_lno is not None else None
        self.text = exc_value.text
        self.offset = exc_value.offset
        self.end_offset = exc_value.end_offset
        self.msg = exc_value.msg
        self._is_syntax_error = True
    elif exc_type and issubclass(exc_type, ImportError) and \
                getattr(exc_value, "name_from", None) is not None:
        wrong_name = getattr(exc_value, "name_from", None)
        suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
        if suggestion:
            self._str += f". Did you mean: '{suggestion}'?"
    elif exc_type and issubclass(exc_type, ModuleNotFoundError) and \
            getattr(exc_value, "name", None) and getattr(exc_value, "_suggestion", []):
        wrong_name = getattr(exc_value, "name", None)
        suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
        if suggestion == wrong_name:
            self._str += ", but it appear in the final result from '__find__'. Is your code wrong?"
        elif suggestion:
            self._str += f". Did you mean: '{suggestion}'?"
    elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
                getattr(exc_value, "name", None) is not None:
        wrong_name = getattr(exc_value, "name", None)
        suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
        if suggestion:
            self._str += f". Did you mean: '{suggestion}'?"
        if issubclass(exc_type, NameError):
            wrong_name = getattr(exc_value, "name", None)
            if wrong_name is not None and wrong_name in sys.stdlib_module_names:
                if suggestion:
                    self._str += f" Or did you forget to import '{wrong_name}'?"
                else:
                    self._str += f". Did you forget to import '{wrong_name}'?"
    if lookup_lines:
        self._load_lines()
    self.__suppress_context__ = \
            exc_value.__suppress_context__ if exc_value is not None else False

    # Convert __cause__ and __context__ to `TracebackExceptions`s, use a
    # queue to avoid recursion (only the top-level call gets _seen == None)
    if not is_recursive_call:
        queue = [(self, exc_value)]
        while queue:
            te, e = queue.pop()
            if (e is not None and e.__cause__ is not None
                    and id(e.__cause__) not in _seen):
                cause = traceback.TracebackException(
                        type(e.__cause__),
                        e.__cause__,
                        e.__cause__.__traceback__,
                        limit=limit,
                        lookup_lines=lookup_lines,
                        capture_locals=capture_locals,
                        max_group_width=max_group_width,
                        max_group_depth=max_group_depth,
                        _seen=_seen)
            else:
                cause = None

            if compact:
                need_context = (cause is None and
                                    e is not None and
                                    not e.__suppress_context__)
            else:
                need_context = True
            if (e is not None and e.__context__ is not None
                    and need_context and id(e.__context__) not in _seen):
                context = traceback.TracebackException(
                        type(e.__context__),
                        e.__context__,
                        e.__context__.__traceback__,
                        limit=limit,
                        lookup_lines=lookup_lines,
                        capture_locals=capture_locals,
                        max_group_width=max_group_width,
                        max_group_depth=max_group_depth,
                        _seen=_seen)
            else:
                context = None

            if e is not None and isinstance(e, BaseExceptionGroup):
                exceptions = []
                for exc in e.exceptions:
                        texc = traceback.TracebackException(
                            type(exc),
                            exc,
                            exc.__traceback__,
                            limit=limit,
                            lookup_lines=lookup_lines,
                            capture_locals=capture_locals,
                            max_group_width=max_group_width,
                            max_group_depth=max_group_depth,
                            _seen=_seen)
                        exceptions.append(texc)
            else:
                exceptions = None

            te.__cause__ = cause
            te.__context__ = context
            te.exceptions = exceptions
            if cause:
                queue.append((te.__cause__, e.__cause__))
            if context:
                queue.append((te.__context__, e.__context__))
            if exceptions:
                queue.extend(zip(te.exceptions, e.exceptions))

traceback.TracebackException.__init__ = new_init

It can pass the test at the module. However, it will change the ModuleNotFoundError object so it need a PEP

I’m no longer sure I understand the purpose of this proposal. It seems like quite a lot of messy and potentially unreliable[1] code, and needs a change to the importer protocols. And this is all just to give a suggestion along the lines of “did you mean X” when an import fails?

If we already had the information available, I’d be supportive of letting the user know. But we don’t. Also, this is the sort of thing where the user would encounter the issue, fix the problem, and never see it again, so there’s not a lot of ongoing benefit here. And in any case, many IDEs offer tooltips and help to pick the right import name, so we’re duplicating what they do.

I personally don’t think it’s worth it.


  1. if the number of iterations that have been needed so far to cover various edge cases is anything to go by ↩

4 Likes

Well, I test for pycharm and vscode. When the code was inputed by yourself from letter to letter, they suggested for it. However, if the code was from ctrl+CV(the old code from online), they didn’t. For example, the pycharm did it better only because it told you where is wrong in the module, the vscode wrosely told about the whole module name.

If there is a third party plugin please tell me.

The number of iteration is only by users. If they make the module name long and many submodule in the import, install too many modules or add too many paths in sys.path. However, they are abnormal behavior.
The normal module can provide the name that in subsubsubsub
module, user don’t need to import the submodule like that. The storage space will forbidden user to install too many modules and before it the device has been nearly unusable. Python has its perfect packages manager system so that adding too many path to sys.path seems to be unnecessary.

Today I had tested for whether the IDE can suggest for that, but it became worse: they even nolonger analyses the fault in import in the old file. So that I think that I didn’t see what you say that “many IDEs offer tooltips and help to pick the right import name” .

To prove that it is wrong, I ask for ChatGPT to give me a code:

import asyncio
from web3 import AsyncWeb3
from web3.providers.async_rpc import AsyncHTTPProvider

async def main():
    # 戛ć»șćŒ‚æ­„ Web3 ćźąæˆ·ç«Ż
    w3 = AsyncWeb3(AsyncHTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID"))

    # äŸ‹ćŠ‚ïŒšèŽ·ć–ćœ“ć‰ćŒșć—é«˜ćșŠ
    block_number = await w3.eth.block_number
    print("Latest block:", block_number)

    # èŽ·ć–æŸäžȘćŒșć—äżĄæŻ
    block = await w3.eth.get_block(block_number)
    print("Block info:", block)

    # èŽ·ć–èŽŠæˆ·äœ™éą
    balance = await w3.eth.get_balance("0x00000000219ab540356cBB839Cbe05303d7705Fa")
    print("Balance:", w3.from_wei(balance, "ether"))

asyncio.run(main())

In fact the import is wrong: “module ‘web3.providers’ has no child module ‘async_rpc’”.
However, the pycharm’s suggestions here:
wrong1

❗ Cannot find reference 'async_rpc' in '__init__.py' :3
❗ Unresolved reference 'AsyncHTTPProvider' :3

In module '__init__.py' create function async_rpc()

Install package async-rpc

Ignore all unresolved attributes of 'web3.providers'

Ignore unresolved reference 'web3.providers.async_rpc'

You call it “help to pick the right import name”?
In fact, some AI used the information that out-of-date so that what would be output may be wrong. What my do will cover the source from copied code.