From 1fc7d812ea0e90ee07d64338a1a9b75d8f5e65b9 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 17 May 2025 02:03:50 -0700 Subject: [PATCH 01/16] add import-untyped-stubs-available, and docu (but not implementation) --- docs/source/error_code_list.rst | 24 ++++++++++++++++++++++-- mypy/errorcodes.py | 3 +++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index d4e2c83323ac..e0bd2a2a8346 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. @@ -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,25 @@ Example: In some cases, these errors can be fixed by installing an appropriate stub package. See :ref:`ignore-missing-imports` for more details. +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/mypy/errorcodes.py b/mypy/errorcodes.py index 785b6166b18b..d2c8bfab1c2b 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -113,6 +113,9 @@ 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" From c5ce82cd461bd12f76c1c00820a98cf7b69750fd Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 17 May 2025 02:23:05 -0700 Subject: [PATCH 02/16] implement import-untyped-stubs-available --- docs/source/running_mypy.rst | 4 +++- mypy/build.py | 7 +++---- mypy/errorcodes.py | 8 +++++++- mypy/errors.py | 6 +++--- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index 9f7461d24f72..352467951723 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 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 d2c8bfab1c2b..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"" @@ -114,7 +117,10 @@ def __hash__(self) -> int: "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 + "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( 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 From cd279e94da0518d0340f536d2f7c08b730dec75b Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 17 May 2025 04:16:08 -0700 Subject: [PATCH 03/16] fix test --- test-data/unit/check-errorcodes.test | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 06c5753db5a7..e65f716c1f21 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -550,7 +550,10 @@ 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. +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] From 9d6282a3010030b44ce75df5dba866cfc86141a0 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 17 May 2025 04:25:30 -0700 Subject: [PATCH 04/16] add an explicit ref/anchor/whatever for import-untyped-stubs-available --- docs/source/error_code_list.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index e0bd2a2a8346..f55d03920259 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -711,6 +711,8 @@ 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] -------------------------------------------------------- From d26ac50bb6fba43e373d1420f999cef7791be2d6 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 17 May 2025 04:40:19 -0700 Subject: [PATCH 05/16] adjust title underline to pass sphinx docs generation without warnings lmao --- docs/source/error_code_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index f55d03920259..8f63bc010332 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -694,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 `). @@ -714,7 +714,7 @@ 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. From 562198bc031301fca5b979538b624a6cf8d41331 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 7 Nov 2025 23:35:09 -0800 Subject: [PATCH 06/16] add a todo --- test-data/unit/check-errorcodes.test | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index e65f716c1f21..e799a5c0acab 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -550,9 +550,12 @@ if int() is str(): # E: Non-overlapping identity check (left operand type: "int [builtins fixtures/primitives.pyi] [case testErrorCodeMissingModule] -# 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. +-- 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 f98aa58d0102a7a7a50a2ef13fc2bac547593b0e Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 7 Nov 2025 23:49:04 -0800 Subject: [PATCH 07/16] manually update the test requirements --- test-requirements.in | 1 + test-requirements.txt | 2 ++ 2 files changed, 3 insertions(+) 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 From bb65ad8ab6a924d21474eefed1f65eacbcb54403 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Fri, 7 Nov 2025 23:49:29 -0800 Subject: [PATCH 08/16] properly type the optional lxml dep --- mypy/report.py | 2 +- mypy/test/testcheck.py | 5 ++++- mypy/test/testcmdline.py | 5 ++++- mypy/test/testreports.py | 8 ++++++-- 4 files changed, 15 insertions(+), 5 deletions(-) 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..2d6d8d1a467b 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -7,6 +7,8 @@ import sys import tempfile from pathlib import Path +from types import ModuleType +from typing import Optional from mypy import build from mypy.errors import CompileError @@ -25,8 +27,9 @@ ) from mypy.test.update_data import update_testcase_output +lxml: Optional[ModuleType] #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..2c8878486dc1 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -10,6 +10,8 @@ import re import subprocess import sys +from types import ModuleType +from typing import Optional from mypy.test.config import PREFIX, test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -19,8 +21,9 @@ normalize_error_messages, ) +lxml: Optional[ModuleType] #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..4664393d33c0 100644 --- a/mypy/test/testreports.py +++ b/mypy/test/testreports.py @@ -3,12 +3,16 @@ from __future__ import annotations import textwrap +from types import ModuleType +from typing import Optional + from mypy.report import CoberturaPackage, get_line_rate from mypy.test.helpers import Suite, assert_equal +lxml: Optional[ModuleType] #lxml is an optional dependency try: - import lxml # type: ignore[import-untyped] + import lxml except ImportError: lxml = None @@ -23,7 +27,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 From 65a3254e5c7ecd543dafb9979b99dc1928235eef Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 8 Nov 2025 00:00:47 -0800 Subject: [PATCH 09/16] punctuation --- docs/source/error_code_list.rst | 2 +- mypy/test/testcheck.py | 2 +- mypy/test/testcmdline.py | 2 +- mypy/test/testreports.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 8f63bc010332..465563f767ef 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -670,7 +670,7 @@ 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`, `import-untyped`, -and `import-untyped-stubs-available` +and `import-untyped-stubs-available`. See :ref:`ignore-missing-imports` for how to work around these errors. diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index 2d6d8d1a467b..ce5c0af77b20 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -27,7 +27,7 @@ ) from mypy.test.update_data import update_testcase_output -lxml: Optional[ModuleType] #lxml is an optional dependency +lxml: Optional[ModuleType] # lxml is an optional dependency try: import lxml except ImportError: diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 2c8878486dc1..24f3937eca42 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -21,7 +21,7 @@ normalize_error_messages, ) -lxml: Optional[ModuleType] #lxml is an optional dependency +lxml: Optional[ModuleType] # lxml is an optional dependency try: import lxml except ImportError: diff --git a/mypy/test/testreports.py b/mypy/test/testreports.py index 4664393d33c0..ce6fa98b8100 100644 --- a/mypy/test/testreports.py +++ b/mypy/test/testreports.py @@ -6,11 +6,10 @@ from types import ModuleType from typing import Optional - from mypy.report import CoberturaPackage, get_line_rate from mypy.test.helpers import Suite, assert_equal -lxml: Optional[ModuleType] #lxml is an optional dependency +lxml: Optional[ModuleType] # lxml is an optional dependency try: import lxml except ImportError: From c2c5a0638f1de2a52b7a03db1a2b1d3dae9c58e2 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 8 Nov 2025 00:43:05 -0800 Subject: [PATCH 10/16] unsafe-fixes --- mypy/test/testcheck.py | 3 +-- mypy/test/testcmdline.py | 3 +-- mypy/test/testreports.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/mypy/test/testcheck.py b/mypy/test/testcheck.py index ce5c0af77b20..1c7002b288c3 100644 --- a/mypy/test/testcheck.py +++ b/mypy/test/testcheck.py @@ -8,7 +8,6 @@ import tempfile from pathlib import Path from types import ModuleType -from typing import Optional from mypy import build from mypy.errors import CompileError @@ -27,7 +26,7 @@ ) from mypy.test.update_data import update_testcase_output -lxml: Optional[ModuleType] # lxml is an optional dependency +lxml: ModuleType | None # lxml is an optional dependency try: import lxml except ImportError: diff --git a/mypy/test/testcmdline.py b/mypy/test/testcmdline.py index 24f3937eca42..eeb9a6abdbc7 100644 --- a/mypy/test/testcmdline.py +++ b/mypy/test/testcmdline.py @@ -11,7 +11,6 @@ import subprocess import sys from types import ModuleType -from typing import Optional from mypy.test.config import PREFIX, test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -21,7 +20,7 @@ normalize_error_messages, ) -lxml: Optional[ModuleType] # lxml is an optional dependency +lxml: ModuleType | None # lxml is an optional dependency try: import lxml except ImportError: diff --git a/mypy/test/testreports.py b/mypy/test/testreports.py index ce6fa98b8100..72682ddcb3c4 100644 --- a/mypy/test/testreports.py +++ b/mypy/test/testreports.py @@ -4,12 +4,11 @@ import textwrap from types import ModuleType -from typing import Optional from mypy.report import CoberturaPackage, get_line_rate from mypy.test.helpers import Suite, assert_equal -lxml: Optional[ModuleType] # lxml is an optional dependency +lxml: ModuleType | None # lxml is an optional dependency try: import lxml except ImportError: From 009e3914f0669116734a7d284ce129ea04252f87 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Sat, 8 Nov 2025 04:14:14 -0500 Subject: [PATCH 11/16] Use a real em dash --- docs/source/running_mypy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index 352467951723..0f6ea02ff2b5 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -394,7 +394,7 @@ 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 — it might even install From d8915a2b1b091ec86091e328c262d26a36ba0305 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 04:16:11 +0000 Subject: [PATCH 12/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/report.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/report.py b/mypy/report.py index 2edd1041b070..270c7d5c54f6 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -31,6 +31,7 @@ LXML_INSTALLED = False else: from lxml import etree + LXML_INSTALLED = True except ImportError: LXML_INSTALLED = False From 77a5cbb9a1bc23b72c6d5440587eebfcf14a414a Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 10 Dec 2025 22:11:51 -0800 Subject: [PATCH 13/16] There is no need to use real em dashes. The rest of the documents do not. --- docs/source/running_mypy.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/running_mypy.rst b/docs/source/running_mypy.rst index 0f6ea02ff2b5..541574efc6a0 100644 --- a/docs/source/running_mypy.rst +++ b/docs/source/running_mypy.rst @@ -394,10 +394,10 @@ 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 — it might even install +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! From 03e2e38970b74bf25e97b1db2e07def86314bed4 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Wed, 10 Dec 2025 22:28:01 -0800 Subject: [PATCH 14/16] Refactor the special ErrorCode function into a static method. I think this also fixes a bug. --- mypy/errorcodes.py | 13 +++++++++++-- mypy/errors.py | 6 +++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 121d9b56ceae..28327b529175 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -34,8 +34,17 @@ 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) + @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: + return False + else: + return possible_parent_code in (possible_child_code, possible_child_code.sub_code_of) def __str__(self) -> str: return f"" diff --git a/mypy/errors.py b/mypy/errors.py index 29d71c7c8729..254192333dd9 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 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 is not None and info.code.is_import_related_code(): + 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 is None or (not info.code.is_import_related_code())) + and ErrorCode.is_code_or_sub_code_of(info.code, IMPORT) and self.has_many_errors() ): # Missing stubs can easily cause thousands of errors about From 681098712b2fd9c9767f6c63d6381d7c5eb75fc6 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 11 Dec 2025 00:38:08 -0800 Subject: [PATCH 15/16] note about the redundant structure --- mypy/errorcodes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 28327b529175..529a5b776f61 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -42,6 +42,7 @@ def is_code_or_sub_code_of( 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) From 7b1cc242287231d6acc37981fef166f5bbee5f79 Mon Sep 17 00:00:00 2001 From: wyattscarpenter Date: Thu, 11 Dec 2025 00:43:43 -0800 Subject: [PATCH 16/16] correct my accidental omittion of a negation --- mypy/errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/errors.py b/mypy/errors.py index 254192333dd9..a7f47abf562c 100644 --- a/mypy/errors.py +++ b/mypy/errors.py @@ -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 ErrorCode.is_code_or_sub_code_of(info.code, IMPORT) + 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