Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions docs/source/error_code_list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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 <installed-packages>`).
Expand All @@ -724,14 +725,35 @@ 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

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]
Expand Down
4 changes: 3 additions & 1 deletion docs/source/running_mypy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <mypy --install-types>` shows a confirmation prompt.
Use :option:`--non-interactive <mypy --non-interactive>` to install all suggested
Expand Down
7 changes: 3 additions & 4 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<ErrorCode {self.code}>"

Expand Down Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions mypy/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion mypy/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
4 changes: 3 additions & 1 deletion mypy/test/testcmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
6 changes: 4 additions & 2 deletions mypy/test/testreports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions test-requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down