diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index d4e2c83323ac..465563f767ef 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -669,7 +669,8 @@ Check for an issue with imports [import] ---------------------------------------- Mypy generates an error if it can't resolve an `import` statement. -This is a parent error code of `import-not-found` and `import-untyped` +This is a parent error code of `import-not-found`, `import-untyped`, +and `import-untyped-stubs-available`. See :ref:`ignore-missing-imports` for how to work around these errors. @@ -693,7 +694,7 @@ See :ref:`ignore-missing-imports` for how to work around these errors. .. _code-import-untyped: Check that import target can be found [import-untyped] --------------------------------------------------------- +------------------------------------------------------ Mypy generates an error if it can find the source code for an imported module, but that module does not provide type annotations (via :ref:`PEP 561 `). @@ -702,7 +703,7 @@ Example: .. code-block:: python - # Error: Library stubs not installed for "bs4" [import-untyped] + # Error: Library stubs not installed for "bs4" [import-untyped-stubs-available] import bs4 # Error: Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped] import no_py_typed @@ -710,6 +711,27 @@ Example: In some cases, these errors can be fixed by installing an appropriate stub package. See :ref:`ignore-missing-imports` for more details. +.. _code-import-untyped-stubs-available: + +Check that import target with known stubs can be found [import-untyped-stubs-available] +--------------------------------------------------------------------------------------- + +Like :ref:`code-import-untyped`, but used when mypy knows there is an appropriate +type stub package corresponding to the library, which you could install. + +Example: + +.. code-block:: python + + # Error: Library stubs not installed for "bs4" [import-untyped-stubs-available] + import bs4 + # Error: Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped] + import no_py_typed + +These errors can be fixed by installing the appropriate +stub package. See :ref:`ignore-missing-imports` for more details. + + .. _code-no-redef: Check that each name is defined once [no-redef] diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index 9f7461d24f72..0f6ea02ff2b5 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -394,10 +394,12 @@ to your mypy command to install all known missing stubs: mypy --install-types This is slower than explicitly installing stubs, since it effectively -runs mypy twice -- the first time to find the missing stubs, and +runs mypy twice — the first time to find the missing stubs, and the second time to type check your code properly after mypy has installed the stubs. It also can make controlling stub versions harder, -resulting in less reproducible type checking. +resulting in less reproducible type checking — it might even install +incompatible versions of your project's non-type dependencies, if the +type stubs require them! By default, :option:`--install-types ` shows a confirmation prompt. Use :option:`--non-interactive ` to install all suggested diff --git a/mypy/build.py b/mypy/build.py index e9c50ce6b224..917c288bc1ef 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2783,11 +2783,10 @@ def module_not_found( msg, notes = reason.error_message_templates(daemon) if reason == ModuleNotFoundReason.NOT_FOUND: code = codes.IMPORT_NOT_FOUND - elif ( - reason == ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS - or reason == ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED - ): + elif reason == ModuleNotFoundReason.FOUND_WITHOUT_TYPE_HINTS: code = codes.IMPORT_UNTYPED + elif reason == ModuleNotFoundReason.APPROVED_STUBS_NOT_INSTALLED: + code = codes.IMPORT_UNTYPED_STUBS_AVAILABLE else: code = codes.IMPORT errors.report(line, 0, msg.format(module=target), code=code) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 785b6166b18b..df7ebd246232 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -34,6 +34,9 @@ def __init__( sub_code_map[sub_code_of.code].add(code) error_codes[code] = self + def is_import_related_code(self) -> bool: + return IMPORT in (self.code, self.sub_code_of) + def __str__(self) -> str: return f"" @@ -113,6 +116,12 @@ def __hash__(self) -> int: IMPORT_UNTYPED: Final = ErrorCode( "import-untyped", "Require that imported module has stubs", "General", sub_code_of=IMPORT ) +IMPORT_UNTYPED_STUBS_AVAILABLE: Final = ErrorCode( + "import-untyped-stubs-available", + "Require that imported module (with known stubs) has stubs", + "General", + sub_code_of=IMPORT, +) NO_REDEF: Final = ErrorCode("no-redef", "Check that each name is defined once", "General") FUNC_RETURNS_VALUE: Final = ErrorCode( "func-returns-value", "Check that called function returns a value in value context", "General" diff --git a/mypy/errors.py b/mypy/errors.py index 69e4fb4cf065..91c6f179e44b 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -11,7 +11,7 @@ from mypy import errorcodes as codes from mypy.error_formatter import ErrorFormatter -from mypy.errorcodes import IMPORT, IMPORT_NOT_FOUND, IMPORT_UNTYPED, ErrorCode, mypy_error_codes +from mypy.errorcodes import ErrorCode, mypy_error_codes from mypy.nodes import Context from mypy.options import Options from mypy.scope import Scope @@ -583,7 +583,7 @@ def _add_error_info(self, file: str, info: ErrorInfo) -> None: self.error_info_map[file].append(info) if info.blocker: self.has_blockers.add(file) - if info.code in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND): + if info.code is not None and info.code.is_import_related_code(): self.seen_import_error = True def get_watchers(self) -> Iterator[ErrorWatcher]: @@ -630,7 +630,7 @@ def add_error_info(self, info: ErrorInfo) -> None: self.only_once_messages.add(info.message) if ( self.seen_import_error - and info.code not in (IMPORT, IMPORT_UNTYPED, IMPORT_NOT_FOUND) + and (info.code is None or (not info.code.is_import_related_code())) and self.has_many_errors() ): # Missing stubs can easily cause thousands of errors about diff --git a/mypy/report.py b/mypy/report.py index 4a0b965077f6..2db3a622f34b 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -26,7 +26,7 @@ from mypy.version import __version__ try: - from lxml import etree # type: ignore[import-untyped] + from lxml import etree LXML_INSTALLED = True except ImportError: diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index f2b7057d9f20..1c7002b288c3 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -7,6 +7,7 @@ import sys import tempfile from pathlib import Path +from types import ModuleType from mypy import build from mypy.errors import CompileError @@ -25,8 +26,9 @@ ) from mypy.test.update_data import update_testcase_output +lxml: ModuleType | None # lxml is an optional dependency try: - import lxml # type: ignore[import-untyped] + import lxml except ImportError: lxml = None diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 11d229042978..eeb9a6abdbc7 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -10,6 +10,7 @@ import re import subprocess import sys +from types import ModuleType from mypy.test.config import PREFIX, test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -19,8 +20,9 @@ normalize_error_messages, ) +lxml: ModuleType | None # lxml is an optional dependency try: - import lxml # type: ignore[import-untyped] + import lxml except ImportError: lxml = None diff --git a/mypy/test/testreports.py b/mypy/test/testreports.py index f638756ad819..72682ddcb3c4 100644 --- a/mypy/test/testreports.py +++ b/mypy/test/testreports.py @@ -3,12 +3,14 @@ from __future__ import annotations import textwrap +from types import ModuleType from mypy.report import CoberturaPackage, get_line_rate from mypy.test.helpers import Suite, assert_equal +lxml: ModuleType | None # lxml is an optional dependency try: - import lxml # type: ignore[import-untyped] + import lxml except ImportError: lxml = None @@ -23,7 +25,7 @@ def test_get_line_rate(self) -> None: @pytest.mark.skipif(lxml is None, reason="Cannot import lxml. Is it installed?") def test_as_xml(self) -> None: - import lxml.etree as etree # type: ignore[import-untyped] + import lxml.etree as etree cobertura_package = CoberturaPackage("foobar") cobertura_package.covered_lines = 21 diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 06c5753db5a7..e799a5c0acab 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -550,7 +550,13 @@ if int() is str(): # E: Non-overlapping identity check (left operand type: "int [builtins fixtures/primitives.pyi] [case testErrorCodeMissingModule] -from defusedxml import xyz # E: Library stubs not installed for "defusedxml" [import-untyped] \ +-- Note: it was too difficult for me to figure out how to test [import-untyped] here, +-- (ideally, it would!) +-- but testNamespacePkgWStubs does test that, anyway. +-- TODO: can this be done? The specific error message is +-- Skipping analyzing "no_py_typed": module is installed, but missing library stubs or py.typed marker [import-untyped] +-- which apparently was never tested for non-namespace packages before... +from defusedxml import xyz # E: Library stubs not installed for "defusedxml" [import-untyped-stubs-available] \ # N: Hint: "python3 -m pip install types-defusedxml" \ # N: (or run "mypy --install-types" to install all missing stub packages) from nonexistent import foobar # E: Cannot find implementation or library stub for module named "nonexistent" [import-not-found] diff --git a/test-requirements.in b/test-requirements.in index 556edf5077d2..b5f2a10885e5 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -6,6 +6,7 @@ attrs>=18.0 filelock>=3.3.0,<3.20.0 # latest version is not available on 3.9 that we still support lxml>=5.3.0; python_version<'3.15' +lxml-stubs>=0.5.1 psutil>=4.0 pytest>=8.1.0 pytest-xdist>=1.34.0 diff --git a/test-requirements.txt b/test-requirements.txt index 126abd7149e6..390b05f427f7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -26,6 +26,8 @@ librt==0.4.0 # via -r mypy-requirements.txt lxml==6.0.2 ; python_version < "3.15" # via -r test-requirements.in +lxml-stubs>=0.5.1 + # via -r test-requirements.in mypy-extensions==1.1.0 # via -r mypy-requirements.txt nodeenv==1.9.1