diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 034c795e7120..9f3322022e32 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -691,7 +691,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. @@ -715,7 +716,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 `). @@ -724,7 +725,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 @@ -732,6 +733,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..541574efc6a0 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -397,7 +397,9 @@ This is slower than explicitly installing stubs, since it effectively 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 1357047d78a0..9f1734f9f8aa 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2993,11 +2993,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 927cd32f8fe0..529a5b776f61 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -34,6 +34,19 @@ def __init__( sub_code_map[sub_code_of.code].add(code) error_codes[code] = self + @staticmethod + def is_code_or_sub_code_of( + possible_child_code: ErrorCode | None, possible_parent_code: ErrorCode + ) -> bool: + """Check if the first code ⊆ the second code, so to speak. + If None is supplied as the first argument, this is always false. + Again, to quote the assert in ErrorCode above, "Nested subcategories are not supported".""" + if possible_child_code is None: + # This check is pretty much entirely just so we can do type-safe property access later. + return False + else: + return possible_parent_code in (possible_child_code, possible_child_code.sub_code_of) + def __str__(self) -> str: return f"" @@ -116,6 +129,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 f9d952af2297..a7f47abf562c 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 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 ErrorCode.is_code_or_sub_code_of(info.code, IMPORT): 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 not ErrorCode.is_code_or_sub_code_of(info.code, IMPORT) 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 ce6a59a00209..270c7d5c54f6 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -30,7 +30,7 @@ # lxml doesn't support free-threading yet LXML_INSTALLED = False else: - 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 96be1a044d91..b250f29347be 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -8,6 +8,7 @@ import sysconfig import tempfile from pathlib import Path +from types import ModuleType from mypy import build from mypy.errors import CompileError @@ -27,12 +28,13 @@ ) from mypy.test.update_data import update_testcase_output +lxml: ModuleType | None # lxml is an optional dependency try: if sys.version_info >= (3, 14) and bool(sysconfig.get_config_var("Py_GIL_DISABLED")): # lxml doesn't support free-threading yet lxml = None else: - 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 dc72917de788..998ed51666b4 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -11,6 +11,7 @@ import subprocess import sys import sysconfig +from types import ModuleType from mypy.test.config import PREFIX, test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -20,12 +21,13 @@ normalize_error_messages, ) +lxml: ModuleType | None # lxml is an optional dependency try: if sys.version_info >= (3, 14) and bool(sysconfig.get_config_var("Py_GIL_DISABLED")): # lxml doesn't support free-threading yet lxml = None else: - 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 a971f297ec92..01cbd887e5d8 100644 --- a/mypy/test/testreports.py +++ b/mypy/test/testreports.py @@ -5,16 +5,18 @@ import sys import sysconfig 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: if sys.version_info >= (3, 14) and bool(sysconfig.get_config_var("Py_GIL_DISABLED")): # lxml doesn't support free-threading yet lxml = None else: - import lxml # type: ignore[import-untyped] + import lxml except ImportError: lxml = None @@ -29,7 +31,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 b1009f8fe085..a681daa79a2c 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -564,7 +564,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 78d9863dc9e6..0fbe60f09a3a 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -6,6 +6,7 @@ attrs>=18.0 filelock>=3.3.0 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 883930c681a4..a16ef79f88b7 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -26,6 +26,8 @@ librt==0.7.3 ; platform_python_implementation != 'PyPy' # 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