Skip to content
20 changes: 20 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@
from mypy.fixup import fixup_module
from mypy.freetree import free_tree
from mypy.fscache import FileSystemCache
from mypy.known_modules import get_known_modules, reset_known_modules_cache
from mypy.messages import best_matches, pretty_seq
from mypy.metastore import FilesystemMetadataStore, MetadataStore, SqliteMetadataStore
from mypy.modulefinder import (
BuildSource as BuildSource,
Expand Down Expand Up @@ -335,6 +337,7 @@ def build(

# This is mostly for the benefit of tests that use builtins fixtures.
instance_cache.reset()
reset_known_modules_cache()

def default_flush_errors(
filename: str | None, new_messages: list[str], is_serious: bool
Expand Down Expand Up @@ -3189,6 +3192,23 @@ def module_not_found(
code = codes.IMPORT
errors.report(line, 0, msg.format(module=target), code=code)

if reason == ModuleNotFoundReason.NOT_FOUND and not errors.prefer_simple_messages():
top_level_target = target.split(".")[0]
if not top_level_target.startswith("_"):
known_modules = get_known_modules(
manager.find_module_cache.stdlib_py_versions, manager.options.python_version
)
matches = best_matches(top_level_target, known_modules, n=3)
matches = [m for m in matches if m.lower() != top_level_target.lower()]
if matches:
errors.report(
line,
0,
f'Did you mean {pretty_seq(matches, "or")}?',
severity="note",
code=code,
)

dist = stub_distribution_name(target)
for note in notes:
if "{stub_dist}" in note:
Expand Down
174 changes: 174 additions & 0 deletions mypy/known_modules.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""Known Python module names for fuzzy matching import suggestions.

This module provides a curated list of popular Python package import names
for suggesting corrections when a user mistypes an import statement.

Sources:
- Python standard library (typeshed/stdlib/VERSIONS)
- Top 100 PyPI packages by downloads (https://github.com/hugovk/top-pypi-packages)

Note: These are import names, not PyPI package names.
"""

from __future__ import annotations

from typing import Final

from mypy.modulefinder import StdlibVersions

POPULAR_THIRD_PARTY_MODULES: Final[frozenset[str]] = frozenset(
{
# Cloud
"boto3",
"botocore",
"aiobotocore",
"s3transfer",
"s3fs",
"awscli",
# HTTP / Networking
"urllib3",
"requests",
"certifi",
"idna",
"charset_normalizer",
"httpx",
"httpcore",
"aiohttp",
"yarl",
"multidict",
"requests_oauthlib",
"oauthlib",
"h11",
# Typing / Extensions
"typing_extensions",
"annotated_types",
"typing_inspection",
# Core Utilities
"setuptools",
"packaging",
"pip",
"wheel",
"virtualenv",
"platformdirs",
"filelock",
"zipp",
"importlib_metadata",
# Data Science / Numerical
"numpy",
"pandas",
"scipy",
"pyarrow",
# Serialization / Config
"yaml",
"pydantic",
"pydantic_core",
"attrs",
"tomli",
"jsonschema",
"jsonschema_specifications",
"jmespath",
# Cryptography / Security
"cryptography",
"cffi",
"pycparser",
"rsa",
"pyjwt",
"pyasn1",
"pyasn1_modules",
# Date / Time
"dateutil",
"pytz",
"tzdata",
# Google / gRPC
"google",
"grpc",
"grpc_status",
"grpc_tools",
"protobuf",
"googleapis_common_protos",
# Testing
"pytest",
"pluggy",
"iniconfig",
# CLI / Terminal
"click",
"colorama",
"rich",
"tqdm",
# Web Frameworks
"starlette",
# Templates / Markup
"jinja2",
"markupsafe",
"pygments",
"markdown_it",
"mdurl",
# Async
"anyio",
"greenlet",
"aiosignal",
"aiohappyeyeballs",
"frozenlist",
# Database
"sqlalchemy",
# Parsing / XML
"pyparsing",
"et_xmlfile",
# OpenTelemetry
"opentelemetry",
# Other Popular Modules
"six",
"fsspec",
"wrapt",
"propcache",
"rpds",
"pathspec",
"PIL",
"psutil",
"referencing",
"trove_classifiers",
"openpyxl",
"dotenv",
"yandexcloud",
"cachetools",
}
)


_known_modules_cache: frozenset[str] | None = None


def reset_known_modules_cache() -> None:
global _known_modules_cache
_known_modules_cache = None


def get_stdlib_modules(
stdlib_versions: StdlibVersions, python_version: tuple[int, int] | None = None
) -> frozenset[str]:
modules: set[str] = set()
for module, (min_ver, max_ver) in stdlib_versions.items():
if python_version is not None:
if python_version < min_ver:
continue
if max_ver is not None and python_version > max_ver:
continue
top_level = module.split(".")[0]
# Skip private and very short modules to avoid false positives and noise
if top_level.startswith("_") or len(top_level) <= 2:
continue
modules.add(top_level)
return frozenset(modules)


def get_known_modules(
stdlib_versions: StdlibVersions | None = None, python_version: tuple[int, int] | None = None
) -> frozenset[str]:
global _known_modules_cache
if _known_modules_cache is not None:
return _known_modules_cache
modules: set[str] = set(POPULAR_THIRD_PARTY_MODULES)
if stdlib_versions is not None:
modules = modules.union(get_stdlib_modules(stdlib_versions, python_version))
_known_modules_cache = frozenset(modules)
return _known_modules_cache
26 changes: 26 additions & 0 deletions test-data/unit/semanal-errors.test
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,32 @@ main:2: error: Cannot find implementation or library stub for module named "m.n"
main:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
main:3: error: Cannot find implementation or library stub for module named "a.b"

[case testMissingModuleFuzzyMatchThirdParty]
import numpyy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about from numpyy import x -- does it also generate the note? From imports are more common than import x.

[out]
main:1: error: Cannot find implementation or library stub for module named "numpyy"
main:1: note: Did you mean "numpy"?
main:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

[case testMissingModuleFuzzyMatchStdlib]
import ittertools
[out]
main:1: error: Cannot find implementation or library stub for module named "ittertools"
main:1: note: Did you mean "itertools"?
main:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

[case testMissingModuleFuzzyMatchNoSuggestion]
import xyzabc123
[out]
main:1: error: Cannot find implementation or library stub for module named "xyzabc123"
main:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

[case testMissingModuleFuzzyMatchUnderscorePrefix]
import _pytest.fixtures
[out]
main:1: error: Cannot find implementation or library stub for module named "_pytest.fixtures"
main:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

[case testErrorInImportedModule]
import m
[file m.py]
Expand Down